汇编语言程序设计(by fzy)笔记

Posted by Leo on 2023-10-17
Estimated Reading Time 8 Minutes
Words 2.1k In Total

寄存器

8086/8088 CPU的寄存器共有14个,都是16位的寄存器,根据用途分为数据寄存器、段寄存器、地址寄存器和控制寄存器4种类型

8086上一代CPU中的寄存器都是8位的;为保证兼容性,这四个寄存器都可以分为两个独立的8位寄存器使用。

分别为AH、AL、BH、BL、CH、CL、DH、DL,H表示 高字节寄存器(高8位),L表示低字节寄存器(低8位)

  1. 数据寄存器(通用寄存器)

    • ax:累加器寄存器(Accumulator Register),主要作为累加器用,是算术运算的主要寄存器
    • bx:基址寄存器(Base Register)
    • cx:计数器寄存器(Count Register)
    • dx:数据寄存器(Data Register)
  2. 地址寄存器

    • si:源变址寄存器(Source Index Register),可用于存放源缓冲区的偏移地址
    • di:目的变址寄存器(Destination Index Register),可用于存放目的缓冲区的偏移地 址
    • bp:基址指针寄存器(Base Pointer Register),用于指出堆栈区基地址
    • sp:栈指针寄存器(Stack Pointer Register),用于指出堆栈区栈顶的偏移地址
  3. 段寄存器

    • cs
    • ds
    • es
    • ss
  4. 控制寄存器

    • IP:指令指针寄存器(Instruction Pointer Register),指出当前正在执行指令的下一条指令所在单元的偏移地址
    • FR:标志寄存器(Flags Register),注意是否溢出的判断位即可

函数调用过程中栈帧的变化

  1. 调用函数前
    • 参数先入栈(注意顺序是相反的,先入 b,再入 a,从右向左)
    • 返回地址入栈(前一个函数调用完当前函数后应该返回的地址),即将eip压入栈
  2. 进入被调用函数
    • ebp(的值)入栈,即被调用函数保存上一层函数的栈帧基地址,同时将当前esp下移
    • 将当前栈指针(esp)赋值给栈帧基地址寄存器(ebp)
    • 分配被调用函数需要的局部变量空间
  3. 函数执行
  4. 函数返回
    • 清理寄存器和栈空间
    • 将当前栈帧的基地址(ebp)复制给栈顶指针(esp),恢复上一层函数的栈顶位置
    • 将栈顶的值弹出,并存储到 ebp 寄存器,恢复上一层函数的栈帧基地址
    • 弹出返回地址,返回值(如果有的话)放在 eax 寄存器

​ 那么问题来了, 一整个函数调用栈只有一个 esp 和一个 ebp,却有很多不同函数的栈帧,那么怎么分出各个函数的栈帧范围呢?这就需要 ebp。 当父函数执行时,ebp 指向父函数的栈底,此时父函数调用子函数,ebp 需要更新指向子函数的栈底(ebp 寄存器的值改成子函数栈底地址),但是父函数的栈底同时需要被保存,这个地址就被保存在更新后的 ebp 所指向的内存空间,也就是每个函数的栈底空间存储的都是上一个函数的栈底空间的地址,当函数调用结束需要回退 ebp 时,使用这个值去更新 ebp 寄存器的值即可

对应的汇编代码为

1
2
3
4
5
6
7
8
9
10
push ebp
mov ebp, esp

; 执行函数
mov eax, a
add eax, b

mov esp, ebp
pop ebp
ret

下面以一次完整的main函数调用sum函数做说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sum(int _a,int _b)
{
int c=0;
c=_a+_b;
return c;
}

int main()
{
int a=10;
int b=20;
int ret;
ret=sum(a,b);
return 0;
}

Compiler Explorer 得到对应的汇编代码如下:

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
sum(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-20], edi
mov DWORD PTR [rbp-24], esi
mov DWORD PTR [rbp-4], 0
mov edx, DWORD PTR [rbp-20]
mov eax, DWORD PTR [rbp-24]
add eax, edx
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 10
mov DWORD PTR [rbp-8], 20
mov edx, DWORD PTR [rbp-8]
mov eax, DWORD PTR [rbp-4]
mov esi, edx
mov edi, eax
call sum(int, int)
mov DWORD PTR [rbp-12], eax
mov eax, 0
leave
ret

main函数的栈在调用之前如图

image-20231029130645688
  1. 函数参数从右至左入栈

    1
    2
    mov     edx, DWORD PTR [rbp-8]
    mov eax, DWORD PTR [rbp-4]
    image-20231029131304939
  2. 返回地址入栈

    call sum(int, int)指令实际上分两步:① push EIP 将下一条指令入栈保存起来;② esp-4 esp指针下移

    image-20231029132313279
  3. main函数基指针入栈保存

    1
    push ebp
    image-20231029132550429

    mov ebp esp 将esp的值存入ebp也就等于将ebp指向esp

    image-20231029132646377

    sub esp 44H 将esp下移动一段空间创建sum函数的栈栈帧

    image-20231029132731995
  4. 将没有显式地初始化的内存用CCCCCCCC进行填充,将ebxesiedi压入栈中

    image-20231029133124542
  5. 复制传入的参数的值,将sum的局部变量c放入[ebp-4]的空间内

    1
    2
    3
    mov     DWORD PTR [rbp-20], edi
    mov DWORD PTR [rbp-24], esi
    mov DWORD PTR [rbp-4], 0
    image-20231029133516034
  6. 执行函数操作

    image-20231029133711955
  7. 将返回值(变量c所在的地址的内容)放入eax寄存器保存住;将ebp的值赋给esp,也就等于将esp指向ebp,销毁sum函数栈帧

    1
    2
    mov eax,dword ptr [ebp-4] 
    mov esp ebp
    image-20231029134005577
  8. ebp出栈,将栈中保存的main函数的基址赋值给ebp

    1
    pop ebp
    image-20231029134149926

    注:leave指令相当于mov esp ebp加上pop ebp,有时被调用函数没有局部变量,ebp始终等于esp,就不用mov esp ebp

    ret指令相当于pop eip

  9. ret相当于 pop eip 就是把之前保存的函数返回地址(也就是main函数中下一条该执行的指令的地址)出栈

    1
    ret
    image-20231029134310656
  10. add esp,8 ,此时若传入sum函数的参数已经不需要了,我们将esp指针上移

    此时函数整个调用过程就结束了,main函数栈恢复到了调用之前的状态

    image-20231029134459105

寻址方式举例

参考下面的汇编代码

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
int main() {
//int* a = (int *)malloc(8 * sizeof(int));
int* a = NULL;
__asm {
push 0x20; // 要开辟8个int型整数的空间,于是将常数0x20(32的十六进制表示)压入栈顶,作为malloc函数的参数
call malloc; // 调用malloc函数,将参数0x20从栈中弹出,并执行分配内存的操作
add esp, 4; // 释放执行malloc函数时压入栈中的参数,恢复栈顶指针
mov [a], eax; // 将eax寄存器中的返回值(malloc分配空间的地址)存储到指针a所对应的内存地址中
}
printf("%x\n", a);
printf("a[1]=%d\n", a[1]);

//a[1] = 1;
__asm {
mov edi, [a]; // 将指针a的值(数组的地址)加载到edi寄存器中
mov dword ptr [edi + 4], 1017; // 将值1017存储到a的值+4字节偏移,即a[1]处
}
printf("a[1]=%d\n", a[1]); // a[1]=1017

int b = 0;
printf("b=%d\n", b);
// b = a[1]
__asm {
mov edi, [a];
mov eax, dword ptr[edi + 4];
mov[b], eax;
}
printf("b=%d\n", b);
return 0;
}

dword ptr 是汇编指令中的一个操作数大小说明符,用于表示操作数的大小为双字(32位)

分支循环汇编示例

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
__asm {
fun_start:
push ebp; // 保存调用者的基址指针
mov ebp, esp;

mov ebx, grade; // 将变量 grade 的值加载到 ebx 寄存器中
cmp ebx, 60; // 将 ebx 和 60 进行比较
jge elseif; // 如果 ebx 大于等于 60,则跳转到 elseif 标签处

mov eax, 'F'; // 如果条件不满足,将字母等级 'F' 存入 eax 寄存器
jmp fun_end; // 无条件跳转到 fun_end 标签处

elseif:
cmp ebx, 60; // 再次将 ebx 和 60 进行比较
jne elsee; // 如果 ebx 不等于 60,则跳转到 elsee 标签处

mov eax, 'E'; // 如果条件不满足,将字母等级 'E' 存入 eax 寄存器
jmp fun_end; // 无条件跳转到 fun_end 标签处

elsee:
mov eax, 'D'; // 如果以上条件都不满足,将字母等级 'D' 存入 eax 寄存器

fun_end:
mov esp, ebp; // 恢复栈指针到调用者的基址指针
pop ebp; // 弹出调用者的基址指针
ret; // 返回
}

其中,elseifelseefun_end等是汇编代码中的标签(label),用于标记代码中的特定位置,以便于在程序执行过程中进行跳转


本着互联网开源的性质,欢迎分享这篇文章,以帮助到更多的人,谢谢!