[toc]
硬件基础
通用寄存器:
- 8位:AH,AL,CH,CL,DH,DL,BH,BL
- 16位:AX,CX,DX,BX,SP,BP,SI,DI
- 32位:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
指令指针寄存器:EIP
标志寄存器:eflags
状态标志位:
- 进位标志 CF:结果的最高位进位则置1
- 溢出标志 OF:次高位的进位与 CF 异或运算的值就是 OF 的值
- 符号标志 SF:取结果的最高位作为 SF 的值
- 零标志 ZF:结果为0则置1
- 辅助进位标志 AF
- 奇偶标志 DF
控制标志位:
- 方向标志 DF:控制串操作的地址是递增还是递减
- 中断允许标志 IF
- 陷阱标志 TF
内存管理模式
16位:分段机制
- 访问内存时使用逻辑地址,物理地址的计算方式:
段基地址 * 16 + 段内偏移
- 访问不同的内存时,会使用不同的段寄存器来提供段基地址
mov ax,[1000h]
:默认使用DS
作为段寄存器mov ax,[bp]
:含有BP
或SP
的栈底栈顶指针寄存器时,使用SS
作为默认的段寄存器mov ax,[bx]
:默认使用DS
作为段寄存器- 若默认的段寄存器是
DS
,那么这个寄存器可以被修改,如mov ax, ss:[1000h]
- 访问内存时,[ ] 内寄存器的组合是有限的,只能使用
BP
,BX
,SI
,DI
寄存器
- 访问内存时使用逻辑地址,物理地址的计算方式:
32位:保护模式机制
- 段寄存器中保存有段选择子
- 仍然使用分段机制的方式计算地址:
段基地址 * 16 + 段内偏移
,但所有的段基地址都是0,故32位的内存管理模式也称为平坦模式 - 使用内存操作数时,在 [ ] 内可以使用多种寄存器的组合
指令基础
指令的组成:操作码(助记符),操作数
其中操作数只有三种类型:寄存器,立即数和内存操作数
指令原型说明符(用于说明指令的操作数可以是什么类型):
imm
:立即数reg
:寄存器seg
:段寄存器mem
:内存操作数大小说明符(决定指令操作数的大小)
8 - 1字节:
reg8
表示只能接受大小是8位的寄存器,如:AH,BH,CL等mem8
表示只能接受大小是8位的内存操作数,如:byte ptr [1000h]
16 - 字操作数
32 - 双字操作数
指令的两个操作数一般不能同时都是内存操作数
算术运算指令:
- 加:add
- 减:sub
- 乘:mul / imul(有符号乘法指令)
- 除:div / idiv
- 自增:inc
- 自减:dec
位运算
- 按位与:and
- 按位或:or
- 按位异或:xor
- 按位取反:not
- 左移:shl
- 右移:shr
数据传送指令
- 赋值:mov
- 取地址:lea
- 交换两个操作数:xchg
- 数据入栈:push
- 数据出栈:pop
- 标志寄存器入栈:pushf
- 将栈顶值给标志寄存器:popf
- 将所有寄存器入栈:pushad(顺序:eax,ecx,edx,ebx,esp,ebp,esi,edi)
- 将栈中数值给所有通用寄存器:popad(顺序与入栈顺序相反)
32位寻址模式
用于确定指令中使用什么操作数
- 立即数寻址
- 寄存器寻址
- 存储器寻址
- 直接寻址
- 寄存器间接寻址
- 寄存器相对寻址
- 基址变址寻址
- 相对基址变址寻址
- 带比例因子寻址
- 带比例因子相对基址变址寻址
串操作指令
重复前缀:rep / repe / repne
默认操作数:ecx 保存重复的次数
repe / repne 有一个附加条件:ZF 标志位的值也会影响指令是否继续重复
串移动指令(相当于 memcpy):movs
DS : [ESI] -> ES : [EDI] rep movs
串存入指令(相当于 memset):stos
默认操作数:edi,al/ax/eax
1
2
3
4
5004582FD B0 32 MOV AL,0x32 ;串填充的内容0x32('2')
004582FF 8D3D 00904500 LEA EDI,DWORD PTR DS:[0x459000];串填充的目的地址
00458305 B9 10000000 MOV ECX,0x10 ;循环次数
0045830A FC CLD ;将方向标志置0,递增
0045830B F3:AA REP STOS BYTE PTR ES:[EDI]串取出指令:lods
默认操作数:esi,al/ax/eax
将 esi 指向的内存取出存入 al/ax/eax
1
2
3
4
5
6
7
8004582FD 8D35 00904500 LEA ESI,DWORD PTR DS:[0x459000];数组的起始地址
00458303 B9 10000000 MOV ECX,0x10
00458308 33C0 XOR EAX,EAX
0045830A 33D2 XOR EDX,EDX ;将eax,edx清空
0045830C AC LODS BYTE PTR DS:[ESI]
0045830D 90 NOP
0045830E 03D0 ADD EDX,EAX
00458310 ^ E2 FA LOOPD SHORT CKme.0045830C ;跳转到0x0045830C处,并将ecx减1串比较指令(相当于 memcmp):cmps
默认操作数:edi,esi
取出 esi 和 edi 的值进行比较,根据比较结果设置状态标志位,但不保存比较结果
1
2
3
4
5
6
7004582FD 8D35 10904500 LEA ESI,DWORD PTR DS:[0x459010];比较的源地址
00458303 8D3D 20904500 LEA EDI,DWORD PTR DS:[0x459020];比较的目的地址
00458309 B9 10000000 MOV ECX,0x10
0045830E FC CLD
0045830F F3:A6 REPE CMPS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI];循环比较,直到ecx为0或ZF为0时结束
00458311 74 1F JE SHORT CKme.00458332;若相等则跳转
00458313 BB 01000000 MOV EBX,0x1;否则将bx置1
无符号数的大小比较通过进位标志 CF 来判断,若 CF=0,则前者大于后者,否则后者大于前者,再结合 ZF 判断是否相等
有符号数的比较:
结果 | 标记位 |
---|---|
大于 | OF == SF 且 ZF == 0 |
小于 | OF != SF |
大于等于 | OF == SF |
小于等于 | OF != SF 且 ZF == 1 |
repe scas:扫描与指定字符不同的字符串
temp <— AX - SRC
FFFF FFFF - 5 = FFFF FFFA 取反得 0000 0005
控制转移指令
loop 循环指定次数
默认操作:ecx 保存循环次数,每循环一次,ecx 就减1,直到减为0就结束循环
jmp 无条件转移指令
间接修改 eip,使程序转移到其他位置执行代码
函数调用指令:call
调用前先将返回地址(call 指令的下一条指令)入栈
跳转到目标地址
函数返回指令:ret
调用时,将栈顶值作为返回值弹出到 eip
eip 指向哪里,就在哪个地方继续执行代码
ret 立即数
,将栈顶值弹出到 eip,然后 esp 加上立即数
JCC 指令(条件跳转指令)
- 无符号跳转指令
- 有符号跳转指令
- 其他指令
程序基础
- 三大结构
- 顺序结构
- 选择结构
- 循环结构
- 函数
选择结构
if … else if … else
1
2
3
4
5
6
7
8int nDay = 0;
scanf("%d", &nDay);
if(nDay == 1)
printf("周一");
if(nDay == 2)
printf("周二");
if(nDay == 13)
printf("周三");汇编版本:
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
43fun proc
push ebp
mov ebp, esp
;scanf("%d", &nDay)
sub esp, 4 ;为局部变量分配栈空间,ebp-4为nDay的位置
lea eax, [ebp-4] ;eax=&nDay
push eax
push "%d"
call crt_scanf
cmp dword ptr [ebp-4], 1
jne _IF_1
;if(nDay == 1)
push "周一"
call crt_printf
add esp, 4
jmp _ENDIF
_IF_1:
cmp dword ptr [ebp-4], 2
jne _IF_2
;if(nDay == 2)
push "周二"
call crt_printf
add esp, 4
jmp _ENDIF
_IF_2:
cmp dword ptr [ebp-4], 3
jne _IF_3
;if(nDay == 3)
push "周三"
call crt_printf
add esp, 4
jmp _ENDIF
_IF_3:
......
_ENDIF:
mov esp, ebp
pop ebp
fun endpC 语言中使用 {} 来分隔每个语句块
汇编指令是自上至下逐条执行,没有跳转指令是不会跑到其他位置执行代码
汇编中构建一个语句块的方式是:
在语句块前加入一条条件跳转指令
在语句块后加入一条无条件跳转指令
switch … case
1
2
3
4
5
6
7int nDay = 0;
scanf("%d", &nDay);
switch(nDay){
case 1: printf("周一"); break;
case 2: printf("周二"); break;
case 3: printf("周三"); break;
}汇编版本:
- 和 if 语句的版本一样
- 使用跳转表
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.386
.model flat, stdcall
option casemap:none
include msvcrt.inc
includelib msvcrt.lib
.const
pszD db'%d', 0
pszTips1 db'周一', 0dh, 0ah, 0
pszTips2 db'周二', 0dh, 0ah, 0
pszTips3 db'周三', 0dh, 0ah, 0
.code
fun proc
push ebp
mov ebp, esp
sub esp, 4 ;为局部变量分配栈空间
;scanf("%d", &nDay)
lea eax, [ebp-4] ;eax=&nDay
push eax
push offset pszD
call crt_scanf
;若输入值大于3则函数结束
cmp dword ptr [ebp-4], 3
jg _ENDIF
mov eax, [ebp-4]
dec eax ;nDay = nDay-1
;jmptab dd _IF_0, _IF_1, _IF_2
;通过使用输入值作为下标,从数组中索引出一个地址,再跳转到此地址
jmp dword ptr [jmptab+eax*4]
;在代码中定义数据:dword jmptab[3] = { _IF_0, _IF_1, _IF_2 }
;编译后 _IF_0标签就会编译成代码的地址
jmptab dd _IF_0, _IF_1, _IF_2
_IF_0:
;if(nDay == 1)
push offset pszTips1
call crt_printf
add esp, 4
jmp _ENDIF
_IF_1:
push offset pszTips2
call crt_printf
add esp, 4
jmp _ENDIF
_IF_2:
push offset pszTips3
call crt_printf
add esp, 4
jmp _ENDIF
_ENDIF:
mov esp, ebp
pop ebp
ret
fun endp
main:
call fun
ret
end main
循环结构
while
1
2
3
4
5int i = 0;
while(i < 100){
printf("%d\n", i);
++i;
}汇编版本:通过条件跳转指令来模拟
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.386
.model flat, stdcall
option casemap:none
include msvcrt.inc
includelib msvcrt.lib
.const
pszD db'%d', 0dh, 0ah, 0
.code
fun proc
;由于没有参数,故可以不需开辟栈帧,将局部变量保存在ebx,不能存在eax,因为printf函数也会用到eax,会改变其值,使程序一直循环,eax是易失性寄存器
sub esp, 4
mov dword ptr [esp], 0 ;i = 0
mov ebx, [esp]
;while(i < 100)
_WHILE:
cmp ebx, 100
jge _END ;若比较结果大于等于0,即i大于等于100就跳出循环
push ebx
push offset pszD
call crt_printf
add esp, 8
;++i
inc ebx
jmp _WHILE
_END:
add esp, 4
ret
fun endp
main:
call fun
ret
end main
do … while
1
2
3
4
5int i = 0;
do{
printf("%d\n", i);
++i;
}while(i < 100);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.386
.model flat, stdcall
option casemap:none
include msvcrt.inc
includelib msvcrt.lib
.const
pszD db'%d', 0dh, 0ah, 0
.code
fun proc
sub esp, 4
mov dword ptr [esp], 0 ;i = 0
mov ebx, [esp]
_DOWHILE:
push ebx
push offset pszD
call crt_printf
add esp, 8
inc ebx
cmp ebx, 100
jge _END
jl _DOWHILE
_END:
add esp, 4
ret
fun endp
main:
call fun
ret
end mainfor 循环结构和 while 循环一样
1
2
3for(int i = 0; i < 100; i++){
printf("%d\n", i);
}
函数
调用约定
调用约定 | 传参方式 | 平衡栈方式 |
---|---|---|
cdecl(C 调用约定) | 从右往左 | 函数外平衡(调用者平衡),add esp, xx |
stdcall(标准调用约定) | 从右往左 | 函数内平衡(被调者平衡),ret xx |
fastcall(快速调用约定) | 前2个参数依次使用 ecx,edx 来传递,后续参数通过栈传递(从右往左依次入栈) | 函数内平衡(被调者平衡),ret xx |
thiscall(对象调用约定) | 通过 ecx 来保存 this 指针,参数从右往左依次入栈 | 函数内平衡(被调者平衡),ret xx |
调用约定制约在代码中传参的方式和平衡栈的方式
在汇编中调用函数前需要搞清楚这个函数使用什么调用约定,否则参数就无法正确传递
总结:
参数是通过栈来传递的,有时是直接
push
到栈中,有时也可先sub esp
拉高栈顶,再使用mov [esp]
的方式将实参赋值到栈中,总之能将实参放入栈中的合适位置就可以、esp 栈顶高地址的位置一般都是被使用过的,不应随意修改,而 esp 低地址的位置一般未被使用,可以用来保存新数据
函数调用完毕后需要平衡栈
函数返回值一般约定使用 eax 来保存
函数栈帧
实参通过栈传递后,在函数内部的定位
1
2
3
4
5
6
7
8
9
10
11
12
13
14;定义一个函数
;void fun(int n1, int n2, int n3)
fun proc
push ebp;要使用ebp保存当前栈顶的值,因此需要先将寄存器的值备份起来
mov ebp, esp;将原栈顶位置保存到ebp中,这样即使栈顶发生浮动也不会影响通过ebp+固定偏移的方式去定位到栈内形参
......
;离开函数前需要恢复在函数内部使用的寄存器的值
;恢复esp和ebp的值
mov esp,ebp;在函数开头,将esp的值保存到ebp中,而ebp在整个函数中是不会被修改的,故esp的值就可使用ebp来恢复
pop ebp;在函数开头,ebp的值保存在栈中,因此可直接pop出ebp原来的值
ret
fun endp
使用局部变量
- 在打开栈帧后,需要给局部变量开辟栈空间
- 通过
sub esp
,所有局部变量的字节数 - esp 的低地址位置一般未被使用,可以用来保存新数据,故没将数据往上抬,栈空间内的数据就可能被 push 或 call 指令的操作覆盖
- 通过
- 通过
ebp-4
得到第一个局部变量的偏移
易失性寄存器
一般进入函数后,函数内部使用寄存器前需要备份寄存器的值,离开函数时,会将寄存器的值恢复,但有些寄存器不会恢复,这些不会恢复的寄存器称为易失性寄存器
windows 的 API 函数的易失性寄存器是 eax
C 运行时库函数(crt_xxx 系列函数)的易失性寄存器是 eax,ecx,edx,ebx
结构体
1 | .386 |