[toc]

硬件基础

  1. 通用寄存器:

    • 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
  2. 指令指针寄存器:EIP

  3. 标志寄存器:eflags

    • 状态标志位:

      • 进位标志 CF:结果的最高位进位则置1
      • 溢出标志 OF:次高位的进位与 CF 异或运算的值就是 OF 的值
      • 符号标志 SF:取结果的最高位作为 SF 的值
      • 零标志 ZF:结果为0则置1
      • 辅助进位标志 AF
      • 奇偶标志 DF
    • 控制标志位:

      • 方向标志 DF:控制串操作的地址是递增还是递减
      • 中断允许标志 IF
      • 陷阱标志 TF
  4. 内存管理模式

    • 16位:分段机制

      • 访问内存时使用逻辑地址,物理地址的计算方式:段基地址 * 16 + 段内偏移
      • 访问不同的内存时,会使用不同的段寄存器来提供段基地址
        • mov ax,[1000h]:默认使用 DS 作为段寄存器
        • mov ax,[bp]:含有 BPSP 的栈底栈顶指针寄存器时,使用 SS 作为默认的段寄存器
        • mov ax,[bx]:默认使用 DS 作为段寄存器
        • 若默认的段寄存器是 DS,那么这个寄存器可以被修改,如 mov ax, ss:[1000h]
        • 访问内存时,[ ] 内寄存器的组合是有限的,只能使用 BPBXSIDI 寄存器
    • 32位:保护模式机制

      • 段寄存器中保存有段选择子
      • 仍然使用分段机制的方式计算地址:段基地址 * 16 + 段内偏移,但所有的段基地址都是0,故32位的内存管理模式也称为平坦模式
      • 使用内存操作数时,在 [ ] 内可以使用多种寄存器的组合

指令基础

  1. 指令的组成:操作码(助记符),操作数

    其中操作数只有三种类型:寄存器,立即数和内存操作数

  2. 指令原型说明符(用于说明指令的操作数可以是什么类型):

    • imm :立即数

    • reg:寄存器

    • seg :段寄存器

    • mem:内存操作数

    • 大小说明符(决定指令操作数的大小)

      • 8 - 1字节:

        reg8 表示只能接受大小是8位的寄存器,如:AH,BH,CL等

        mem8 表示只能接受大小是8位的内存操作数,如:byte ptr [1000h]

      • 16 - 字操作数

      • 32 - 双字操作数

  3. 指令的两个操作数一般不能同时都是内存操作数

  4. 算术运算指令:

    • 加:add
    • 减:sub
    • 乘:mul / imul(有符号乘法指令)
    • 除:div / idiv
    • 自增:inc
    • 自减:dec
  5. 位运算

    • 按位与:and
    • 按位或:or
    • 按位异或:xor
    • 按位取反:not
    • 左移:shl
    • 右移:shr
  6. 数据传送指令

    • 赋值:mov
    • 取地址:lea
    • 交换两个操作数:xchg
    • 数据入栈:push
    • 数据出栈:pop
    • 标志寄存器入栈:pushf
    • 将栈顶值给标志寄存器:popf
    • 将所有寄存器入栈:pushad(顺序:eax,ecx,edx,ebx,esp,ebp,esi,edi)
    • 将栈中数值给所有通用寄存器:popad(顺序与入栈顺序相反)

32位寻址模式

用于确定指令中使用什么操作数

  1. 立即数寻址
  2. 寄存器寻址
  3. 存储器寻址
    1. 直接寻址
    2. 寄存器间接寻址
    3. 寄存器相对寻址
    4. 基址变址寻址
    5. 相对基址变址寻址
    6. 带比例因子寻址
    7. 带比例因子相对基址变址寻址

串操作指令

  1. 重复前缀:rep / repe / repne

    默认操作数:ecx 保存重复的次数

    repe / repne 有一个附加条件:ZF 标志位的值也会影响指令是否继续重复

  2. 串移动指令(相当于 memcpy):movs

    DS : [ESI] -> ES : [EDI] rep movs

  3. 串存入指令(相当于 memset):stos

    默认操作数:edi,al/ax/eax

    1
    2
    3
    4
    5
    004582FD    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]

    1572935965242

  4. 串取出指令:lods

    默认操作数:esi,al/ax/eax

    将 esi 指向的内存取出存入 al/ax/eax

    1
    2
    3
    4
    5
    6
    7
    8
    004582FD    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

    1572940498963

  5. 串比较指令(相当于 memcmp):cmps

    默认操作数:edi,esi

    取出 esi 和 edi 的值进行比较,根据比较结果设置状态标志位,但不保存比较结果

    1
    2
    3
    4
    5
    6
    7
    004582FD    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

    1572941081723

无符号数的大小比较通过进位标志 CF 来判断,若 CF=0,则前者大于后者,否则后者大于前者,再结合 ZF 判断是否相等

有符号数的比较:

结果 标记位
大于 OF == SF 且 ZF == 0
小于 OF != SF
大于等于 OF == SF
小于等于 OF != SF 且 ZF == 1
  1. repe scas:扫描与指定字符不同的字符串

    temp <— AX - SRC

    FFFF FFFF - 5 = FFFF FFFA 取反得 0000 0005

控制转移指令

  1. loop 循环指定次数

    默认操作:ecx 保存循环次数,每循环一次,ecx 就减1,直到减为0就结束循环

  2. jmp 无条件转移指令

    间接修改 eip,使程序转移到其他位置执行代码

  3. 函数调用指令:call

    调用前先将返回地址(call 指令的下一条指令)入栈

    跳转到目标地址

  4. 函数返回指令:ret

    调用时,将栈顶值作为返回值弹出到 eip

    eip 指向哪里,就在哪个地方继续执行代码

    ret 立即数,将栈顶值弹出到 eip,然后 esp 加上立即数

JCC 指令(条件跳转指令)

  1. 无符号跳转指令
  2. 有符号跳转指令
  3. 其他指令

程序基础

  • 三大结构
    • 顺序结构
    • 选择结构
    • 循环结构
  • 函数

选择结构

  1. if … else if … else

    1
    2
    3
    4
    5
    6
    7
    8
    int 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
    43
    fun 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 endp
    • C 语言中使用 {} 来分隔每个语句块

    • 汇编指令是自上至下逐条执行,没有跳转指令是不会跑到其他位置执行代码

    • 汇编中构建一个语句块的方式是:

      在语句块前加入一条条件跳转指令

      在语句块后加入一条无条件跳转指令

  2. switch … case

    1
    2
    3
    4
    5
    6
    7
    int nDay = 0;
    scanf("%d", &nDay);
    switch(nDay){
    case 1: printf("周一"); break;
    case 2: printf("周二"); break;
    case 3: printf("周三"); break;
    }

    汇编版本:

    1. 和 if 语句的版本一样
    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
    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

循环结构

  1. while

    1
    2
    3
    4
    5
    int 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
  1. do … while

    1
    2
    3
    4
    5
    int 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 main
  2. for 循环结构和 while 循环一样

    1
    2
    3
    for(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

调用约定制约在代码中传参的方式和平衡栈的方式

在汇编中调用函数前需要搞清楚这个函数使用什么调用约定,否则参数就无法正确传递

总结:

  1. 参数是通过栈来传递的,有时是直接 push 到栈中,有时也可先 sub esp 拉高栈顶,再使用 mov [esp] 的方式将实参赋值到栈中,总之能将实参放入栈中的合适位置就可以、

    esp 栈顶高地址的位置一般都是被使用过的,不应随意修改,而 esp 低地址的位置一般未被使用,可以用来保存新数据

  2. 函数调用完毕后需要平衡栈

  3. 函数返回值一般约定使用 eax 来保存

函数栈帧

  1. 实参通过栈传递后,在函数内部的定位

    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

    1573028086214

使用局部变量

  1. 在打开栈帧后,需要给局部变量开辟栈空间
    1. 通过 sub esp,所有局部变量的字节数
    2. esp 的低地址位置一般未被使用,可以用来保存新数据,故没将数据往上抬,栈空间内的数据就可能被 push 或 call 指令的操作覆盖
  2. 通过 ebp-4 得到第一个局部变量的偏移

易失性寄存器

一般进入函数后,函数内部使用寄存器前需要备份寄存器的值,离开函数时,会将寄存器的值恢复,但有些寄存器不会恢复,这些不会恢复的寄存器称为易失性寄存器

windows 的 API 函数的易失性寄存器是 eax

C 运行时库函数(crt_xxx 系列函数)的易失性寄存器是 eax,ecx,edx,ebx

结构体

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
.386
.model flat,stdcall
option casemap:none
include msvcrt.inc
includelib msvcrt.lib

MYSTRUCT struct ;定义一个结构体
nNum dd ?
szBuff db 100 dup(?)
MYSTRUCT ends

PERSON struct ;定义一个结构体
age dd ?
sex db ?
szName db 100 dup(?)
PERSON ends
.const
pszHello db'Hello', 0
pszHello15PB db'Hello 15PB', 0
.code
fun proc
;定义一个结构体局部变量
;PERSON personobj
;MYSTRUCT myobj
sub esp, sizeof(PERSON)+sizeof(MYSTRUCT)
;PERSON --> esp
;MYSTRUCT --> esp+sizeof(PERSON)

mov ecx, esp ;ecx存PERSON的首地址
;personobj.age = 18
mov dword ptr [ecx + PERSON.age], 18
;personobj.sex = 'M'
mov byte ptr [ecx + PERSON.sex], 'M'
;personobj.szName = "Hello"
;strcpy(personobj.szName, "Hello")
push offset pszHello
lea eax, [ecx + PERSON.szName]
push eax
call crt_strcpy
add esp, 8

lea esi, [esp + sizeof(PERSON)] ;esi存MYSTRUCT的首地址
;myobj.nNum = 20
mov dword ptr [esi + MYSTRUCT.nNum], 20
;myobj.szBuff = "Hello 15PB"
;strcpy(myobj.szBuff, "Hello 15PB")
push offset pszHello15PB
lea eax, [esi + MYSTRUCT.szBuff]
push eax
call crt_strcpy
add esp, 8

ret
fun endp
main:
call fun
end main