前言 本博客用于复习IA32架构下微软宏汇编(MASM)的基础知识。作为初学者,本人水平有限,若内容存在疏漏或错误,恳请读者斧正
开发环境 使用Visual Studio 2017 Community
程序格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 .386 ;使用.386指令集 .model flat,stdcall ;flat内存模式 调用子程序使用stdcall comment/* flat: 所有段(代码段、数据段、堆栈段)都使用相同的32位地址空间 不需要使用段寄存器(CS, DS, SS等),所有内存访问都使用32位偏移地址 stdcall: 参数从右向左压入堆栈 被调用函数负责清理堆栈(与cdecl不同,cdecl是调用者清理堆栈) */ option casemap:none ;大小写敏感 .const ;常量区 fm db "%s" ARR_LEN = 1 ;也可以这样声明常量 .data ;全局变量区 x db 0 ;1bytes y dw 0 ;字;2bytes z dd 0 ;双字;4bytes arr dd 50 dup(0) ;50个连续的值为零的4字节空间 .code main proc ;main程序段 _start:: main endp end _start ;end后的部分就是程序入口,在这里,_start就是程序入口
常用寄存器 EAX 通用寄存器之一,存放函数的返回值
EBX,ECX,EDX寄存器布局与此类似
EBX 通用寄存器之一
ECX 通用寄存器之一,存放循环次数
也用于字符串操作(如 rep stosb)、移位指令(如 shl eax, cl`)
EDX 通用寄存器之一
ESI 通用寄存器之一
在字符串/内存操作中默认指向源数据地址
EDI 在字符串/内存操作中默认指向目标地址
ESP 栈顶指针寄存器,存放栈顶的地址
PUSH会先递减 ESP,再写入数据;POP 会读取数据后递增 ESP
EBP 通常用作栈帧基址 (函数内通过 [EBP+offset] 访问局部变量和参数)
EIP 程序计数器寄存器,存放下一条指令的地址
常用标志位 CF(Carry Flag) 无符号数溢出标志
add/sub存在进位/借位时,CF置为1
ZF(Zero Flag) 运算结果为0时,置为1
SF(Sign Flag) 运算结果的最高位 0:正数 1:负数
PF(Parity Flag) 结果低8位的 1 的个数是否为偶数
0:奇校验
1:偶校验
OF(Overflow Flag) 有符号数溢出标志
DF(Direction Flag) 字符串操作方向标志
0:正向
1:反向
常用指令 inc-自增 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ;对全局变量自增 .data ;全局变量 x dd 0 ;x=0 .code main proc ;main程序段 _start:: inc x ;x = 1 xor eax,eax ret main endp end _start ;end后的部分就是程序入口,在这里,_start就是程序入口 ;------------------------------------------------------ .data ;全局变量 .code main proc ;main程序段 _start:: mov eax,0 ;eax:0x00000000 inc eax ;eax:0x00000001 xor eax,eax ret main endp end _start ;end后的部分就是程序入口,在这里,_start就是程序入口
dec-自减 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ;对全局变量自减 .data ;全局变量 x dd 1 ;x=1 .code main proc ;main程序段 _start:: dec x ;x = 0 xor eax,eax ret main endp end _start ;end后的部分就是程序入口,在这里,_start就是程序入口 ;------------------------------------------------------ .data ;全局变量 .code main proc ;main程序段 _start:: mov eax,1 ;eax:0x00000001 dec eax ;eax:0x00000000 xor eax,eax ret main endp end _start ;end后的部分就是程序入口,在这里,_start就是程序入口
mov 1 2 3 4 5 6 7 8 9 10 mov dst,src ;将立即数传送到寄存器 mov eax,1 ;将寄存器中的值传送到另一寄存器;注意两个寄存器的大小要一致(8bits->8bits;16bits->16bits;32bits->32bits) mov eax,ebx mov al,ah ;mov的两个操作数不能都是内存操作数(比如.data段中的数据)或立即数 mov [esp+1],[esp+2] ;错误 mov [esp+1],1 ;错误 mov x,y ;错误
movzx-零拓展传送 高位补零 ,无论源操作数的符号位(最高位)是 0 还是 1,高位全部填充 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .code main proc _start:: mov al,-1 ;eax:??????ff movzx ax,al ;al拓展到ax eax:????00ff movzx,eax,al ;al扩展到eax eax:000000ff movzx eax,ax ;ax拓展到eax xor eax,eax ret main endp end _start
初始的al
movzx ax,al后(注意ah的变化)
movzx eax,ax后
movsx-符号拓展传送 源操作数高位是1就补1,高位是0就补0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .code main proc _start:: mov al,-1 movsx ax,al ;al拓展到ax eax:????ffff movsx eax,al ;al拓展到eax eax:ffffffff movsx eax,ax ;ax拓展到eax xor eax,eax ret main endp end _start
初始的al
movsx ax,al后
mov ax,eax后
sub 1 2 3 4 5 6 7 8 9 10 11 12 13 sub dst,src ;寄存器中的值减去立即数 sub eax,1 ;将寄存器的值减去另一寄存器中的值 sub eax,ebx ;将内存中的值减去寄存器中的值 sub x,ebx sub [esi+4],eax ;sub的两个操作数不能都是内存操作数(比如.data段中的数据)或立即数 sub [esp+1],[esp+2] ;错误 sub [esp+1],1 ;错误 sub x,y ;错误
add 1 2 3 4 5 6 7 8 9 10 11 12 add dst,src ;将立即数加到寄存器中 add eax,1 ;将寄存器的值加到寄存器中 add eax,ebx ;将寄存器中的值加到内存 add x,ebx add [esi+4],eax ;add的两个操作数不能都是内存操作数(比如.data段中的数据)或立即数 add [esp+1],[esp+2] ;错误 add [esp+1],1 ;错误 add x,y ;错误
adc-带进位的加法 cf标志位: 加法(ADD/ADC) :如果运算结果的最高位产生进位,CF = 1,否则 CF = 0。
减法(SUB/SBB) :如果运算需要借位(被减数 < 减数),CF = 1,否则 CF = 0。
移位/循环指令(SHL/SHR/ROL/ROR等) :存放被移出的位。
比较指令(CMP) :同减法,CMP A, B 等价于 SUB A, B(但不保存结果,只改标志位)
1 2 add sum,ebx ;sum的前4个字节 adc sum+4,0 ;给sum的后四个字节加上进位 <-> 相当于sum+4 = sum+4+0+cf
mul-无符号数乘法 隐含使用 AL/AX/EAX 作为被乘数
1 2 3 4 5 mul bl ;结果位于ax mul bx ;结果:DX:AX (高16:低16) mul ebx ;结果:EDX:EAX (高32:低32)
imul-有符号数乘法 单操作数 与mul相同
双操作数
dest=dest*src
dest只能是16位或32位寄存器,src可以是通用寄存器/内存操作数/立即数
三操作数
dest = src1*src2
dest只能是16位或32位寄存器,src1可以是通用寄存器和内存,不能是立即数,src2只能是立即数
div-无符号整数除法
oprd可以是通用寄存器和内存操作数,但不能是立即数
8位:
被除数:ax,商:al,余数ah
16位:
被除数:eax,商:ax,余数dx
32位:
被除数:edx:eax,商:eax,余数edx
移位指令 移位位数放在cl或者8位立即数
shl逻辑左移,sal算术左移 这俩其实是一条机器指令,只是方便记忆变成两条
1 2 3 4 SHL r/m, imm8 ; 左移立即数位 SHL r/m, CL ; 左移CL寄存器指定的位数 SAL r/m, imm8 ; 右移立即数位 SAL r/m, CL ; 右移CL寄存器指定的位数
每左移一位,右边补一位0,移出的最高位放在cf
shr逻辑右移 1 2 SHR r/m, imm8 ; 右移立即数位 SHR r/m, CL ; 右移CL寄存器指定的位数
右移一位,左边补一位0,移出的最低位放在cf
sar算术右移 1 2 SAR r/m, imm8 ; 右移立即数位 SAR r/m, CL ; 右移CL寄存器指定的位数
右移一位,左边符号位不变,移出的最低位放在cf
符号扩展指令 CBW 将al中8位有符号数扩展到ax中
若al最高位为0,ah全补0,否则全补1
1 2 3 4 5 6 7 8 9 .data x1 db 0fh y1 db 0ffh mov al,x1 ;eax:??????0f cbw ;eax:????000f mov al,y1 ;eax:??????ff cbw ;eax:????ffff
CWD 将ax中16位有符号数扩展到dx:ax中
若ax最高位为0,dx全补0,否则全补1
1 2 3 4 5 6 7 8 9 .data x1 dw 0000fh y1 dw 0ffffh mov ax,x1 ;eax:????000f edx:???????? cwd ;eax:????000f edx:????0000 mov ax,y1 ;eax:????ffff edx:???????? cwd ;eax:????ffff edx:????ffff
CDQ eax扩展到eax:edx
若eax最高位为0,edx全补0,否则全补1
1 2 3 4 5 6 7 8 9 .data x1 dd 00000000fh y1 dd 0f0000000h mov eax,x1 ;eax:00000000f edx:???????? cdq ;eax:00000000f edx:00000000 mov eax,y1 ;eax=f0000000 edx:???????? cdq ;eax=f0000000 edx:ffffffff
CWDE ax扩展到eax
若ax最高位为0,eax高16位全补0,否则全补1
1 2 3 4 5 6 7 8 9 .data x1 dw 0000fh y1 dw 0ffffh mov ax,x1 ;eax:????000f cwde ;eax:0000000f mov ax,y1 ;eax:????f000 cwde ;eax:fffff000
串操作指令 movsb 将esi指向地址的数据复制到edi指向的地址(一次1字节)
然后根据DF标志位自动递增或递减ESI和EDI:
DF=0时:ESI和EDI递增1
DF=1时:ESI和EDI递减1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .data str1 db "Hello World!",00h len dd $-str1 str2 db 00h lea esi,str1 lea edi,str2 movsb ;str2:H ;与rep结合,一次移动一块 mov ecx,len lea esi,str1 lea edi,str2 rep movsb
movsw 将esi指向地址的数据复制到edi指向的地址(1次2字节)
然后根据DF标志位自动递增或递减ESI和EDI:
DF=0时:ESI和EDI递增1
DF=1时:ESI和EDI递减1
movsd 将esi指向地址的数据复制到edi指向的地址(1次4字节)
然后根据DF标志位自动递增或递减ESI和EDI:
DF=0时:ESI和EDI递增1
DF=1时:ESI和EDI递减1
stosb 需要配合rep来实现对一块连续内存进行串填充,填充次数存放在ecx寄存器中,填充值放在al/ax/eax中,填充到edi指向的地址(涉及目的地 ,使用edi)
字符串操作方向由df标志位实现(正向:df=0 反向:df=1)
rep stosb/stosw之前应使用cld指令先重置df
stosw stosd lea 取地址并传送
1 2 3 4 5 6 7 .data arr db 50 dub(0) .code main proc lea esi,arr ;取arr的首地址传送到esi中 main endp end main
等价于
1 2 3 4 5 6 7 .data arr db 50 dub(0) .code main proc mov esi,offset arr ;取arr的首地址传送到esi中 offset:通用偏移地址获取 main endp end main
xchg 交换内存/寄存器的值
1 2 xchg op1,op2 ;op1和op2中至少有一个是寄存器操作数
and
dest = dest & src
or
dest = dest | src
xor
dest = dest xor src
test 与and类似,但是不送到dest中,仅影响标志位
转移指令 jmp 无条件转移
转移到标签
1 2 3 test1: mov eax,offset test1 jmp test1
寄存器间接转移
1 2 3 test1: mov eax,offset test1 jmp eax ;转移到eax中的地址
je/jz 当zf=0时转移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .code main proc _start:: test1: mov eax,1 mov ebx,1 cmp eax,ebx jz test1 ;此时相当于死循环 写成je也是相同的效果 xor eax,eax ret main endp end _start
ja/jnbe 无符号数比较,高于(不低于等于)时转移(cf=0且zf=0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 .code main proc _start:: test1: mov eax,1 mov ebx,4 cmp ebx,eax ja test1 ;跳转到test1 jmp end_if end_if: xor eax,eax ret main endp end _start ;------------------------ .code main proc _start:: test1: mov eax,1 mov ebx,4 cmp eax,ebx ja test1 jmp end_if ;跳转到程序结束 end_if: xor eax,eax ret main endp end _start
jb/jnae 无符号数比较,低于(不高于等于)时转移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 .code main proc _start:: test1: mov eax,1 mov ebx,4 cmp ebx,eax jb test1 jmp end_if ;跳转到程序结束 end_if: xor eax,eax ret main endp end _start ;------------------------ .code main proc _start:: test1: mov eax,1 mov ebx,4 cmp eax,ebx jb test1 ;跳转到test1 jmp end_if end_if: xor eax,eax ret main endp end _start
jo 溢出转移(OF=1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .data c1 dd 07fffffffh ;int_max .code main proc _start:: test1: mov eax,1 mov ebx,4 add eax,c1 jo test1 jmp end_if end_if: xor eax,eax ret main endp end _start
jno 不溢出转移(OF=0)
js 为负转移(sf=1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .code main proc _start:: test1: mov eax,1 mov ebx,4 add eax,c1 ;0xffffffff = -1 为负 js test1 ;转移到test1 jmp end_if end_if: xor eax,eax ret main endp end _start
jns 为正转移(sf=0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .code main proc _start:: test1: mov eax,1 mov ebx,4 add ebx,c1 ;2=0x00000010 jns test1 ;转移到test1 jmp end_if end_if: xor eax,eax ret main endp end _start
jg/jnle 有符号数比较,大于/不小于等于时转移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .code main proc _start:: test1: mov eax,-1 mov ebx,-4 cmp eax,ebx jg test1 ;-1 > -4 转移 jmp end_if end_if: xor eax,eax ret main endp end _start
jl/jnge 有符号数比较,小于/不大于等于时转移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .code main proc _start:: test1: mov eax,-1 mov ebx,-4 cmp ebx,eax jl test1 ;-4 < -1 转移 jmp end_if end_if: xor eax,eax ret main endp end _start
jcxz cx=0时转移
jecxz ecx=0时转移
寻址方式 寄存器寻址
基址寻址 常用于堆栈
1 2 3 push ebp mov ebp,esp mov eax,[ebp+4] ;基址寻址
立即数寻址
变址寻址
相对寻址
寄存器间接寻址 访问指针指向的变量
顺序程序设计 eg:有三个长度分别为1、2、4个字节的数据,编写程序求和存放到内存中
程序1 三个数据均为无符号数,求和的结果考虑进位的存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 .data ;全局变量 a db 1 b dw 2 c1 dd 0ffffffffh ;无符号数 sum dd 0,0 ;因为要考虑进位存储,需要额外再开4字节空间 .code main proc _start:: movzx ax,a ;a零拓展到ax add ax,b ;b加到ax中 movzx eax,ax ;ax拓展到eax中 add eax,c1 ;发生溢出,CF标志位被置为1 mov sum,eax adc sum+4,0 ;进位传送到高4字节 相当于sum+4 = sum+4+0+CF xor eax,eax ret main endp end _start
程序2 三个数据均为有符号数,求和的结果不考虑进位的存储(进位直接丢掉)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .data ;全局变量 a db 1 b dw 2 c1 dd 0ffffffffh ;int:-1 uint:最大值 sum dd 0 .code main proc _start:: movsx ax,a ;a有符号拓展到ax add ax,b ;b加到ax中 movsx eax,ax ;ax有符号拓展到eax中 add eax,c1 ;2 xor eax,eax ret main endp end _start
思考 1.用户如何自定义超过系统事先定义好的数据类型的长度?(比如在C语言中实现128位整数)
使用连续的等长内存空间来分段存储,根据地址偏移进行分段运算和进位传递
1 2 _int64 arr[2 ]; _int32 arr[4 ];
2.不同寻址方式对编写程序的作用
作用:不同的寻址方式有不同的程序执行效率,程序可读性与可维护性
简单分支程序设计 汇编中是通过条件/无条件转移指令实现简单的分支程序(if-else结构)
程序1 实现逻辑或的逻辑短路
对应的c语言代码
1 2 3 4 5 6 7 #include <stdio.h> int main () { int a=5 ,b=6 ,c=7 ,d=8 ,m=2 ,n=2 ; (m=a<b)||(n=c>d); printf ("%d\t%d" ,m,n); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 .data a dd 5 b dd 6 c1 dd 7 d dd 8 m dd 2 n dd 2 .code main proc _start:: mov eax,a cmp eax,b jl alb ;a小于b,exp1 || exp2中的exp1为true,短路 mov eax,0 mov m,eax mov eax,c1 cmp eax,d jle cleb ;给n赋值0 mov eax,1 mov n,eax ;给n赋值1 jmp end_proc alb: ;a小于b,exp1 || exp2中的exp1为true,短路 mov eax,1 mov m,eax jmp end_proc cleb: mov eax,0 mov n,eax jmp end_proc end_proc: xor eax,eax ret main endp end _start
程序2 实现逻辑与的逻辑短路
对应的c语言代码
1 2 3 4 5 6 7 #include <stdio.h> int main () { int a=5 ,b=6 ,c=7 ,d=8 ,m=2 ,n=2 ; (m=a<b)&&(n=c>d); printf ("%d\t%d" ,m,n); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 .data a dd 5 b dd 6 c1 dd 7 d dd 8 m dd 2 n dd 2 .code main proc _start:: mov eax,a cmp eax,b jge ageb ;大于等于,第一个表达式为假,短路 mov eax,1 mov m,eax mov eax,c1 cmp eax,d jle cleb ;小于等于,第2个表达式为假 mov eax,1 mov n,eax jmp end_proc ageb: mov eax,0 mov m,eax ;给m赋值为0 jmp end_proc cleb: mov eax,0 mov n,eax jmp end_proc end_proc: xor eax,eax ret main endp end _start
思考 1.简述分支语句的实现原理(注意标志位在其中的作用)
根据标志位值的变化和相应的跳转指令
2.简述逻辑运算短路的特征
逻辑或: exp1 || exp2
当exp1结果为真时,整体就为真,不再执行exp2就直接跳转到对应分支
逻辑与 exp1 && exp2
当exp1结果为假时,整体就为假,不再执行exp2就直接跳转到对应分支
地址表分支程序设计 在简单分支程序中,有太多的标签与各种各样的跳转指令,分支的不可预测性强,这样会破坏流水线技术
但是在地址表分支程序中,所有分支的地址都位于一个地址表中,其实只需要根据间接寻址,使用一条jmp指令就能转移到对应分支
原C程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> int main () { int grade = 90 ; switch (grade / 10 ) { case 9 : printf ("excellence" ); break ; case 8 : printf ("good" ); break ; case 7 : printf ("average" ); break ; case 6 : printf ("pass" ); break ; default : printf ("fail" ); } return 0 ; }
汇编实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 .data grade dd 90 adr_table dd offset case6,offset case7,offset case8,offset case9 .code main proc _start:: xor edx,edx mov eax,grade mov bx,10 div bx sub eax,6 ;获取和6的差值,即后面的偏移量 cmp eax,0 jl default ;低于60 mov eax,[adr_table+eax*4] ;根据偏移量找到对应分支的地址,解引用后传送到eax中 jmp eax ;转移到对应分支 case6:: mov eax,6 case7:: mov eax,7 case8:: mov eax,8 case9:: mov eax,9 default: end_proc: xor eax,eax ret main endp end _start
思考 1.采用地址表和不采用地址表有什么区别
地址表是一种空间换时间的算法,时间复杂度为O(1),但是空间复杂度达到了O(n)
通过连续内存来存储分支的地址,减少了程序中各种转移指令的使用,不仅使程序可读性更强,同时分支可预测,在分支之间转移的时间少,利于流水线模式
区别:
采用地址表:程序可读性更强,直接使用地址偏移量跳转,更加高效;但是空间开销较高,比较适合条件连续的分支
不采用地址表:程序可读性没那么好,容易逻辑混乱。但是空间开销较小
2.如果分支常量值不连续,还可以使用地址表吗
可以,没有值的地方填充0或者指定值(空间换时间)
循环程序设计 eg:
编写程序实现C语言函数void *memset(void* s,int ch,size_t n),将指定的内存中连续N个字节填写成指定的内容,要求:
每次填写一个字节
每次填写一个字
分别用LOOP指令、串操作指令、条件(无条件)转移指令分别实现以上的操作
loop指令实现 循环次数由ecx寄存器决定
1.每次填写一个字节
定义数据
1 2 3 4 fill_var = 03ch ;将填充值定义为常量 .data arr db 10 dup(0) ;n dup(x) 给连续n个空间赋值x arr_len dd $-arr ;$表示当前地址,$-arr表示填充的字节数有多少
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .code main proc _start:: mov al,fill_var ;一次写入一个字节,填充值传送到al中 mov ecx,arr_len ;循环次数传送到ecx中 lea esi,arr ;取arr的首地址传送到esi中 memset: mov [esi],al ;将填充值传送到esi指向的地址,完成一字节的填充 inc esi ;指针向后移一个字节 loop memset ;循环 xor eax,eax ret main endp end _start
2.每次填写一个字
定义数据
1 2 3 4 fill_var = 03ch ;填充值 .data arr dw 10 dup(0) ;字数组 arr_len dd ($-arr)/2 ;填充字数
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .code main proc _start:: mov ax,fill_var mov ecx,arr_len lea esi,arr memset: mov [esi],ax add esi,2 ;因为一次写入一个字(2个字节),这里地址要加2 loop memset xor eax,eax ret main endp end _start
串操作指令实现 需要配合rep来实现对一块连续内存进行串填充,填充次数存放在ecx寄存器中,填充值放在al/ax/eax中,填充到edi指向的地址(涉及目的地 ,使用edi)
字符串操作方向由df标志位实现(正向:df=0 反向:df=1)
rep stosb/stosw之前应使用cld指令先重置df
stosb 将al中的值写入[edi]中,即一次写入一个字节
定义数据
1 2 3 4 fill_var = 03ch ;填充值 .data arr db 10 dup(0) ;字节数组 arr_len dd ($-arr) ;填充字节数
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .code main proc _start:: mov al,fill_var mov ecx,arr_len ;重复次数 lea edi,arr ;操作的首地址一定要放在edi中 cld ;清空df位 rep stosb xor eax,eax ret main endp end _start
stosw 将ax中的值写入[edi]中,即一次写入一个字(2字节)
定义数据
1 2 3 4 fill_var = 03ch ;填充值 .data arr dw 10 dup(0) ;字数组 arr_len dd ($-arr)/2 ;填充字数
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .code main proc _start:: mov ax,fill_var mov ecx,arr_len lea edi,arr cld rep stosw xor eax,eax ret main endp end _start
条件/无条件转移指令实现 类似与C语言中的
1 2 3 for (int i = len;i>=0 ;--i){ }
一次写入一个字节
定义数据
1 2 3 4 fill_var = 03ch ;填充值 .data arr db 10 dup(0) ;字节数组 arr_len dd ($-arr) ;填充字节数
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .code main proc _start:: mov al,fill_var mov ecx,arr_len lea esi,arr memset: cmp ecx,0 jle end_loop dec ecx mov [esi],al inc esi jmp memset end_loop: xor eax,eax ret main endp end _start
一次写入一个字
定义数据
1 2 3 4 fill_var = 03ch ;填充值 .data arr dw 10 dup(0) ;字数组 arr_len dd ($-arr)/2 ;填充字数
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .code main proc _start:: mov ax,fill_var mov ecx,arr_len lea esi,arr memset: cmp ecx,0 jle end_loop dec ecx mov [esi],al add esi,2 jmp memset end_loop: xor eax,eax ret main endp end _start
实现冒泡排序 C语言实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int k = n - 1 ;for (int i = 0 ; i < n; ++i){ for (int j = 0 ; j < k; ++j) { if (arr[j] > arr[j + 1 ]) { int tmp = arr[j]; arr[j] = arr[j + 1 ]; arr[j + 1 ] = tmp; } } k--; }
关键点:
内外两层循环,内循环次数比外循环少一次
内循环中有一个临时变量用于交换值
汇编实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 fill_var = 03ch ;填充值 .data arr db 10,9,8,7,6,5,4,3,2,1 ;字节数组 arr_len dd ($-arr) ;填充字节数 .code main proc _start:: lea esi,arr mov ecx,arr_len outer: cmp ecx,0 jbe end_loop push ecx ;内循环也需要ecx作为循环次数,因此需要压入堆栈保护起来 push esi ;esi指向首地址,但是内循环中需要esi向后移动,为了防止找不到首地址,也需要将esi压入堆栈保护 inner: mov bl,[esi] ;用寄存器模拟临时变量 mov dl,[esi+1] ;用寄存器模拟临时变量 cmp bl,dl ;if (arr[j] > arr[j + 1]) ja swap jmp no_swap swap: mov [esi],dl mov [esi+1],bl no_swap: inc esi ;指向下一字节 loop inner pop esi pop ecx ;平衡堆栈 loop outer end_loop: xor eax,eax ret main endp end _start
初始数组:
排序后:
思考 loop指令和串操作指令的性能对比,必须通过循环实现的程序如何提高性能?
使用串操作指令的性能优于loop指令
提高性能:
1.使用更宽的数据进行操作,减少循环次数
2.确保内存对齐
3.避免使用loop,使用更高效的指令(比如串操作指令)
循环程序和分支程序的关系?
循环程序和分支程序都依赖于条件判断和跳转指令,不同的是,循环程序是反复多次跳转,而分支程序跳转后变不再反复跳转回来
LOOP的双重循环例子,理解系统如何存储临时变量?
将ecx压入堆栈中进行保护,防止内层循环改变ecx的值导致外层循环的结果错误
高级语言中break和continue的实现?
continue:跳转到当前循环的标签
break:跳转到循环外的另一个标签
子程序设计 格式 1 2 3 4 5 6 7 子程序名 proc push ebp ;栈帧基址 mov ebp,esp pop ebp ;平衡堆栈 ret 子程序名 endp
调用
call指令会将下一条指令(在这里就是add esp,8)的地址压入堆栈中(称为返回地址,子程序ret后就会返回到这个位置),进入子程序后,堆栈内部情况如下
重点 能判断出堆栈的状态,可以用excel画当前堆栈示意图
调用约定 stdcall 参数从右至左压入堆栈,子程序(被调函数)负责平衡堆栈
在汇编中传入参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 .code addxy proc push ebp mov ebp,esp mov eax,[ebp+8] add eax,[ebp+12] pop ebp ret 8 addxy endp main proc _start:: push ebp mov ebp,esp push y ;先压y push x ;再压x call addxy xor eax,eax pop ebp ret main endp end _start
cdecl 参数从右至左压入堆栈,调用者负责平衡堆栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 .code addxy proc push ebp mov ebp,esp mov eax,[ebp+8] add eax,[ebp+12] pop ebp ret addxy endp main proc _start:: push ebp mov ebp,esp push y ;先压y push x ;再压x call addxy add esp,8 ;平衡堆栈 xor eax,eax pop ebp ret main endp end _start
fastcall 前两个参数用 ECX/EDX,子程序(被调函数)负责平衡堆栈
例题 eg:编写汇编语言子程序,实现C表达式SUM=X+Y的功能
函数的参数传递采用寄存器实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 .data x1 dd 5 y1 dd 6 .code addxy proc push ebp mov ebp,esp add eax,ebx pop ebp ret addxy endp main proc _start:: push ebp mov ebp,esp mov eax,x1 mov ebx,y1 call addxy xor eax,eax pop ebp ret main endp end _start
函数的参数传递采用堆栈实现,要求函数的形式为int addxy(int ,int)[传值调用]
C语言函数声明
汇编实现
压栈顺序为 y -> x
堆栈示意图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 .data x1 dd 5 y1 dd 6 .code addxy proc push ebp mov ebp,esp mov eax,[ebp+8] add eax,[ebp+12] pop ebp ret 8 addxy endp main proc _start:: push ebp mov ebp,esp push y1 push x1 call addxy xor eax,eax pop ebp ret main endp end _start
函数的参数传递采用堆栈实现,要求函数的形式为void addxy(int ,int,int*)[传址调用]
C语言函数声明
1 void addxy (int x,int y,int * sum) ;
汇编实现
压栈顺序: int* sum -> y -> x
堆栈示意图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 .data x1 dd 5 y1 dd 6 sum dd 0 .code addxy proc push ebp mov ebp,esp mov eax,[ebp+8] add eax,[ebp+12] mov [esi],eax ;*sum = x+y pop ebp ret 12 addxy endp main proc _start:: push ebp mov ebp,esp lea esi,sum push esi push y1 push x1 call addxy xor eax,eax pop ebp ret main endp end _start
结构体传参
在汇编中定义结构体
1 2 3 4 5 6 7 8 结构体名 struct x1 dd ? y1 dd ? sum dd ? 结构体名 ends .data 变量名 结构体名 <1,2,0> ;{x1=1,y1=2,sum=0}
传参时,应该是传址调用,因此我们需要传入结构体变量的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 dataseg struct x1 dd ? y1 dd ? sum dd ? dataseg ends .data data dataseg <5,6,0> .code addxy proc push ebp mov ebp,esp mov eax,[esi] ;(&data)->x传送到eax add eax,[esi+4] ;(&data)->y加到eax mov [esi+8],eax ;eax中的值传送到(&data)->sum pop ebp ret 4 ;平衡堆栈 addxy endp main proc _start:: push ebp mov ebp,esp lea esi,data ;&data push esi call addxy xor eax,eax pop ebp ret main endp end _start
5)改正下面程序的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ;调用程序 … Mov bx,10 Mov cx,20 Call fun Add bx,cx Add ax,bx … ;被调用程序 Fun proc Xor ax,ax Mov cx,5 Mov bx,1 Shl bx,cx Ret Fun endp
1.bx,cx寄存器在调用程序段和被调用程序段都被使用,却没有压进堆栈中进行保护,导致子程序段中破坏了它们
2.左移位指令shl使用错误,移位次数应该放在cl中而不是cx
3.子程序的结果并没有存到ax中,导致子程序的结果丢失,ax中的值始终是0
思考 1.标准C函数参数结合顺序从右至左是什么意思
答:参数压入堆栈的顺序从右至左
2.栈在程序中的作用
答:保护寄存器,存储临时变量,传递参数
3.从机器执行的角度理解标准C中传值和传地址是什么意思
答: 传值:压入堆栈的是原变量的副本,在堆栈中不会影响原变量的值,影响的只是原变量的副本
传址:压入堆栈的是原变量的地址,在堆栈中可以改变地址指向的值从而影响原变量
4.系统调用是指什么,怎么实现的
答:程序调用操作系统提供的函数,通过引用操作系统提供的动态库(.lib)
5.为什么函数只能返回一个值
答:函数返回值通过eax寄存器,但是eax寄存器只有一个
6.函数调用时如何转到被调用的函数,又是如何返回的
答:call指令相当于两条指令:
1.将call指令的下一条指令地址压入堆栈作为返回地址
2.jmp 子程序
7.函数的入口地址是什么概念,为什么C语言可以通过指向函数的指针调用函数
答:函数首条指令的地址;函数指针中存放的是函数首条指令的地址,而汇编中调用函数就相当于 call [函数首地址]
8.编写子程序时为什么要注意寄存器的保护
答:在子程序中使用寄存器,要先压入堆栈进行保护,使用完毕后再弹出,这是因为,子程序中直接使用寄存器会破坏其中的值,特别是这个寄存器如果在调 用者中还要再次使用,如果值被破坏,那么程序最后的执行结果就会出错
9.Call指令和RET的指令的作用是什么,RET后面跟的常数是什么意思,为什么要在后面跟常数?
答:
call指令:调用子程序同时将调用程序的下一条指令地址压入堆栈
ret指令:程序段执行完毕,返回操作系统
常数:告诉操作系统平衡堆栈需要几个字节
为什么:需要平衡堆栈,避免堆栈不平衡导致栈溢出
系统调用 在vs2017中,因为我们创建的项目是C++空项目,所以项目属性中已经帮我们引用了对应的系统调用的库
声明函数 声明或者调用的时候应当注意参数通常声明为DWORD的数据就可以了,但是要注意,如果在C|C++中声明的是指针,调用的时候需要加上ADDR/offset
以ReadFile为例
1 2 3 4 5 6 7 BOOL ReadFile ( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ) ;
在汇编中声明
1 2 3 4 5 6 ReadFile proto hFile:dword, lpBuf:dword, notr:dword, lobr:dword, lol:dword
为什么使用proto呢?
使用proto声明就可以忽略函数的修饰名
如果使用extrn
1 EXTRN __imp__ReadFile@20:PROC
函数名太繁琐
调用函数 使用invoke(更方便) 1 invoke WriteFile,_hout$[ebp],offset msg,msg_len,addr _cWritten$[ebp],00h
使用call 1 2 3 4 5 6 7 8 9 push 00h ; lpOverlapped (最后一个参数先压栈) lea eax, _cWritten$[ebp] ; lpNumberOfBytesWritten 的地址 push eax movsx eax,msg_len ; nNumberOfBytesToWrite(若msg_len是变量,需用 offset) push eax lea eax, msg ; lpBuffer push eax push _hout$[ebp] ; hFile call WriteFile
使用call更加麻烦,需要自己将参数压入堆栈(但是不用手动平衡堆栈)
例题 将下面的C语言代码改写成汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include "stdafx.h" #include <windows.h> HANDLE hStdout, hStdin; int main (void ) { LPSTR lpszPrompt1 = "Type a line and press Enter, or q to quit: " ; CHAR chBuffer[256 ]; DWORD cRead, cWritten; hStdin = GetStdHandle(STD_INPUT_HANDLE); hStdout = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdin == INVALID_HANDLE_VALUE || hStdout == INVALID_HANDLE_VALUE) { return 1 ; } while (1 ) { if (!WriteFile( hStdout, lpszPrompt1, lstrlenA(lpszPrompt1), &cWritten, NULL )) { return 1 ; } if (!ReadFile( hStdin, chBuffer, 255 , &cRead, NULL )) break ; if (chBuffer[0 ] == 'q' ) break ; } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 .386 .model flat,stdcall option casemap:none GetStdHandle proto nStdHandle:dword ReadFile proto hFile:dword, lpBuf:dword, notr:dword, lobr:dword, lol:dword WriteFile proto hFile:dword, lpBuf:dword, notr:dword, lobr:dword, lol:dword STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 INVALID_HANDLE_VALUE = 0FFFFFFFFH BUF_LEN = 255 .data msg db "Type a line and press Enter, or q to quit: ",00dh,00ah ;00dh,00ah 为\r\n msg_len db $-msg chBuffer db BUF_LEN dup(0) .code main proc _start:: _hout$ = -16 _hin$ = -12 _cRead$ = -8 _cWritten$ = -4 push ebp mov ebp,esp sub esp,16 ;给局部变量留空间 invoke GetStdHandle,STD_INPUT_HANDLE mov _hin$[ebp],eax invoke GetStdHandle,STD_OUTPUT_HANDLE mov _hout$[ebp],eax mov ebx,_hin$[ebp] cmp ebx,INVALID_HANDLE_VALUE mov ebx,_hout$[ebp] je InvalidError cmp ebx,INVALID_HANDLE_VALUE je InvalidError infloop: invoke WriteFile,_hout$[ebp],offset msg,msg_len,addr _cWritten$[ebp],00h cmp eax,0 je InvalidError invoke ReadFile,_hin$[ebp],offset chBuffer,255,addr _cRead$[ebp],00h cmp eax,0 je NormalEnd lea edi,chBuffer mov al,[edi] cmp al,071h je NormalEnd jmp infloop NormalEnd: add esp,16 xor eax,eax pop ebp ret InvalidError: add esp,16 mov eax,1 pop ebp ret main endp end _start
思考: 系统调用/API是什么,程序员为什么通常要了解特定系统的系统调用/API?
系统调用:是内核提供的底层接口,通过软中断(如 int 0x80)或专用指令(如 syscall)触发
API:应用程序接口,是高级语言对系统调用的封装,可能涉及多个系统调用。
不同的操作系统有不同的系统调用,不了解他们会使自己写的程序不具有跨平台移植性
同时,系统调用是别人造好的轮子;用别人造好的轮子,写程序更加方便高效
什么是HANDLE?有什么用?
句柄,用来标识对象的标识符,用来描述窗口,文件等
句柄隐藏了资源的具体实现细节,应用程序只需通过句柄与资源进行交互,而无需关心资源的存储位置和内部结构,这使得资源管理更加简单和高效
模块化编程 要点 与高级语言类似,将不同的功能拆分到不同的.asm文件中
汇编的主程序入口是end后的标号,所以在子程序模块中的.code段中,end后就不能再跟上标号或者段名
子程序中需要声明子程序段为public,否则其他程序段可能就无法调用该子程序
调用者中使用proto引用子程序,引用格式与声明格式保持一致(proc -> proto)
以addxy为例
addxy.asm声明方法1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .386 .model flat,stdcall option casemap:none .data public addxy ;这样才能在其他文件中被调用 .code addxy proc stdcall x:dword,y:dword push ebp mov ebp,esp mov eax,x add eax,y xor eax,eax pop ebp ret 8 ;stdcall规定由被调用者平衡堆栈 addxy endp end
addxy.asm声明方法2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .386 .model flat,stdcall option casemap:none .data public addxy .code addxy proc _x$ = 8 _y$ = 12 push ebp mov ebp,esp mov eax,_x$[ebp] add eax,_y$[ebp] pop ebp ret 8 ;stdcall规定由被调用者平衡堆栈 addxy endp end
main.asm
对于第一种声明方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .386 .model flat,stdcall option casemap:none addxy proto stdcall x:dword,y:dword ;引用与声明格式一致,最好使用proto引用 .data .code main proc _start:: push ebp mov ebp,esp invoke addxy,4,5 xor eax,eax pop ebp ret main endp end _start
对于第二种声明方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 .386 .model flat,stdcall option casemap:none addxy proto ;或extrn addxy:proto .data x dd 5 y dd 6 .code main proc _start:: push ebp mov ebp,esp push y push x call addxy xor eax,eax pop ebp ret main endp end _start
例题 编写汇编程序完成以下的C语言代码的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int addxy (int x,int y) { return x+y; } int main () { int x; int y; int sum; scanf ("%d" ,&x); scanf ("%d" ,&y); sum=addxy(x,y); printf ("%d" ,sum); return 0 ; }
func.asm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .386 .model flat,stdcall option casemap:none GetStdHandle proto stdcall nStdHandle:dword public func .code func proc stdcall nStdHandle:dword invoke GetStdHandle,nStdHandle ret func endp end
input.asm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 .386 .model flat,stdcall option casemap:none STD_INPUT_HANDLE = -10 INVALID_HANDLE_VALUE = 0FFFFFFFFH BUF_LEN = 255 func proto stdcall nStdHandle:dword ReadFile proto stdcall hFile:dword,lpBuffer:dword,nNumberOfBytesToRead:dword,lpNumberOfBytesToRead:dword,lpOverlapped:dword .data buf db BUF_LEN dup(0) public input .code input proc stdcall var_addr:dword ;局部变量在堆栈中偏移量 _lol$ = -20 _lobr$ = -16 _lobt$ = -12 _buf$ = -8 _hfile$ = -4 push ebp mov ebp,esp sub esp,20 ;获取句柄 invoke func,STD_INPUT_HANDLE mov _hfile$[ebp],eax push esi lea esi,buf ;取buf的地址送入堆栈 mov _buf$[ebp],esi pop esi invoke ReadFile,_hfile$[ebp],addr buf,BUF_LEN,addr _lobr$[ebp],00h mov ecx,_lobr$[ebp] push esi lea esi,buf push ebx push edx mov dl,10 xor eax,eax mov dh,00dh ;将输入的字符串转换为数字 convert: cmp [esi],dh je end_convert xor ebx,ebx mov bl,[esi] sub bl,030h imul dl add eax,ebx inc esi loop convert end_convert: pop edx pop ebx pop esi add esp,20 pop ebp ;将转换结果再传给参数指向的值 mov ebx,[var_addr] mov [ebx],eax ret 4 input endp end
output.asm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 .386 .model flat,stdcall option casemap:none STD_OUTPUT_HANDLE = -11 INVALID_HANDLE_VALUE = 0FFFFFFFFH BUF_LEN = 255 func proto stdcall nStdHandle:dword WriteFile proto stdcall hFile:dword,lpBuffer:dword,nNumberOfBytesToRead:dword,lpNumberOfBytesToRead:dword,lpOverlapped:dword .data buf db BUF_LEN dup(0) public output .code output proc stdcall var:dword _lol$ = -20 _lobw$ = -16 _lobt$ = -12 _buf$ = -8 _hfile$ = -4 push edx push eax push ebx push ecx push esi xor edx,edx xor ecx,ecx xor ebx,ebx mov eax,var mov ebx,10 lea esi,buf ;将数字重新转换成字符,采用压栈逆序转换 convert: cmp eax,0 je popBuf xor edx,edx div ebx add edx,030h push edx inc ecx jmp convert popBuf: pop edx mov [esi],dl inc esi loop popBuf writeFile: mov ecx,00h mov [esi],ecx pop esi pop ecx pop ebx pop eax pop edx push ebp mov ebp,esp sub esp,20 invoke func,STD_OUTPUT_HANDLE mov _hfile$[ebp],eax push esi lea esi,buf mov _buf$[ebp],esi pop esi invoke WriteFile,_hfile$[ebp],addr buf,BUF_LEN,addr _lobw$[ebp],00h add esp,20 pop ebp ret 4 output endp end
addxy.asm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .386 .model flat,stdcall option casemap:none .data public addxy .code addxy proc stdcall x:dword,y:dword,sum:dword ;传址 mov eax,x add eax,y mov ecx,sum mov [ecx],eax ret 12 addxy endp end
main.asm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 .386 .model flat,stdcall option casemap:none addxy proto stdcall x:dword,y:dword,sum:dword input proto stdcall var_addr:dword output proto stdcall var:dword .data .code main proc _start:: _x$ = -12 _y$ = -8 _sum$ = -4 push ebp mov ebp,esp sub esp,12 invoke input,addr _x$[ebp] invoke input,addr _y$[ebp] invoke addxy,_x$[ebp],_y$[ebp],addr _sum$[ebp] invoke output,_sum$[ebp] add esp,12 xor eax,eax pop ebp ret main endp end _start