汇编作业报告

First Post:

Last Update:

汇编作业报告

[TOC]

引言

本报告旨在分析一个简单的 C 语言程序,分别在 Visual Studio 和 GCC 环境下编译生成的汇编代码。我们将通过对比两者的汇编指令,深入探讨编译器生成的优化和差异。重点分析栈操作、寄存器的变化以及函数调用机制等方面。

作业说明

分析的 C 语言源码见 2,是简单的 add 函数和 printf 输出 ,我会直接分析 visual stdio 中的汇编代码(一些 visual stdio 特有的汇编操作会进行标出),之后也会和我用 ubuntu 中的 gcc 编译然后 ida 查看的汇编代码进行比较。

C 程序源码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int main() {
int i = 10;
int j = 16;
printf("%d\n", add(i, j));

return 0;
}

每条语句对寄存器的影响情况

visual stdio 版

main 函数汇编代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

int main() {
004118C0 push ebp
004118C1 mov ebp,esp
004118C3 sub esp,0D8h
004118C9 push ebx
004118CA push esi
004118CB push edi
004118CC lea edi,[ebp-18h]
004118CF mov ecx,6
004118D4 mov eax,0CCCCCCCCh
004118D9 rep stos dword ptr es:[edi]
004118DB mov ecx,offset _D5122A33_作业@cpp (041C008h)
004118E0 call @__CheckForDebuggerJustMyCode@4 (041132Fh)
004118E5 nop



汇编 影响 受影响的寄存器取值 注释
push ebp esp-4 栈的初始化
mov ebp, esp ebp ebp 指向 esp 处 栈的初始化
sub esp,0D8h esp esp 减少 D8 为栈腾出空间
push ebx esp esp-4
push esi esp esp-4
push edi esp esp-4
lea edi, [ebp-18h] edi edi = ebp-18h 右边源操作数地址加载进左边 edi
mov ecx,6 ecx ecx = 6 计数器 6 次
mov eax,0CCCCCCCCh eax eax = CCCCCCCC
rep stos dword ptr es: [edi] es: [edi] eax 的值被依次赋值给 es 处的 edi 偏移处(CCCCCCCC) ecx 次循环赋值
mov ecx, offset _D5122A33_作业@cpp (041C008h) ecx 变量的地址被赋值给 ecx 方便后续使用
call @__CheckForDebuggerJustMyCode@4 (041132Fh) esp esp-4 这个是 visual stdio 的调试辅助函数
nop


main 内汇编代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int i = 10;
004118E6 mov dword ptr [i],0Ah
int j = 16;
004118ED mov dword ptr [j],10h
printf("%d\n", add(i, j));
004118F4 mov eax,dword ptr [j]
004118F7 push eax
004118F8 mov ecx,dword ptr [i]
004118FB push ecx
004118FC call add (04112D0h)
00411901 add esp,8
00411904 push eax
00411905 push offset string "%d\n" (0417B30h)
0041190A call _printf (04110D2h)
0041190F add esp,8


汇编 影响 受影响的寄存器取值 注释
mov dword ptr [i],0Ah 变量 i i = 10
mov dword ptr [j],10h 变量 j j = 16
mov eax, dword ptr [j] eax eax = 16
push eax esp esp-4
mov ecx, dword ptr [i] ecx ecx = 10
push ecx esp esp-4
call add (04112D0h) esp esp-4 push 返回地址加 jmp 到 add
add esp,8 esp esp+8 清理栈
push eax esp esp-4 将操作数压入栈中
push offset string “%d\n” (0417B30h) esp esp-4 将字符串 "%d\n" 的地址(即 0417B30h)压入栈中
call _printf (04110D2h) esp esp-4 push 返回地址加 jmp 到 add
add esp,8 esp esp+8 清理栈 恢复到 printf 函数调用前


return汇编代码分析

1
2
3
4
5
6
7
8
9
10
11
12
return 0;
00411912 xor eax,eax
}
00411914 pop edi
00411915 pop esi
00411916 pop ebx
00411917 add esp,0D8h
0041191D cmp ebp,esp
0041191F call __RTC_CheckEsp (041124Eh)
00411924 mov esp,ebp
00411926 pop ebp
00411927 ret
汇编 影响 受影响的寄存器取值 注释
xor eax, eax eax eax = 0 异或快速清零
pop edi esp esp+4
pop esi esp esp+4
pop ebx esp esp+4
add esp,0D8h esp es0+0d8h 恢复栈原始空间
cmp ebp, esp 查栈指针是否和栈帧基指针匹配,确保没有栈溢出或栈损坏
call __RTC_CheckEsp (041124Eh) 检测栈是否被破坏(这两步都是 visual stdio 添加的 RTC 检查)
mov esp, ebp 恢复栈
pop ebp 弹出 ebp 并把 ebp 值存到 ebp 中
ret 弹出返回地址并继续执行


add函数汇编代码分析

以下是跳转的 add 函数汇编代码

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
int add(int a, int b) {
00411790 push ebp
00411791 mov ebp,esp
00411793 sub esp,0C0h
00411799 push ebx
0041179A push esi
0041179B push edi
0041179C mov edi,ebp
0041179E xor ecx,ecx
004117A0 mov eax,0CCCCCCCCh
004117A5 rep stos dword ptr es:[edi]
004117A7 mov ecx,offset _D5122A33_作业@cpp (041C008h)
004117AC call @__CheckForDebuggerJustMyCode@4 (041132Fh)
004117B1 nop
return a + b;
004117B2 mov eax,dword ptr [a]
004117B5 add eax,dword ptr [b]
}
004117B8 pop edi
004117B9 pop esi
004117BA pop ebx
004117BB add esp,0C0h
004117C1 cmp ebp,esp
004117C3 call __RTC_CheckEsp (041124Eh)
004117C8 mov esp,ebp
004117CA pop ebp
004117CB ret
汇编 影响 受影响的寄存器取值 注释
push ebp esp esp-4
mov ebp, esp ebp ebp = esp
sub esp,0C0h esp esp-0C0h
push ebx esp esp-4
push esi esp esp-4
push edi esp esp-4
mov edi, ebp edi edi = ebp
xor ecx, ecx ecx ecx = 0 执行 0 次操作
mov eax,0CCCCCCCCh eax eax = CCCCCCCC
rep stos dword ptr es: [edi] es: [edi] eax 的值被依次赋值给 es 处的 edi 偏移处(CCCCCCCC) ecx 次循环赋值
mov ecx, offset _D5122A33_作业@cpp (041C008h) ecx ecx = 041C008h 把该变量的地址存进 ecx
call @__CheckForDebuggerJustMyCode@4 (041132Fh) esp esp-4 push 返回地址加 jmp 到 visual stdio 的调试辅助函数
nop
mov eax, dword ptr [a] eax eax = a
add eax, dword ptr [b] eax eax = b
pop edi esp esp+4
pop esi esp esp+4
pop ebx esp esp+4
add esp,0C0h esp esp+0C0h 恢复栈原始空间
cmp ebp, esp 查栈指针是否和栈帧基指针匹配,确保没有栈溢出或栈损坏
call __RTC_CheckEsp (041124Eh) esp esp-4 检测栈是否被破坏(这两步都是 visual stdio 添加的 RTC 检查)
mov esp, ebp esp esp = ebp 恢复栈
pop ebp esp esp+4 弹出 ebp 并把 ebp 值存到 ebp 中
ret 弹出返回地址并继续执行





gcc 编译版

gcc 编译步骤

​ 我使用WSL(Windows Subsystem for Linux)在我的win10系统上安装了Ubuntu子系统,并且使用vscode的wsl插件进行编辑

(https://cdn.jsdelivr.net/gh/mchxchimeng/picodemo/img/20250406145414270.png)

使用gcc32位编译语言代码指令得到.elf文件

gcc -m32 example.c -o example

然后用ida打开如图

由于gcc大部分和visual stdio是相似的(出于时间考虑) gcc编译的分析我交给了chatgpt来做 如下

汇编代码 影响的寄存器 寄存器值变化 注释
lea ecx, [esp+4] ecx ecx 指向传入参数的位置 计算传入参数的位置
and esp, 0FFFFFFF0h esp esp 对齐为 16 字节边界 栈对齐操作
push dword ptr [ecx-4] ecx, esp ecx 压入栈,esp 向下移动 4 字节 压栈传入参数
push ebp ebp, esp ebp 压入栈,esp 向下移动 4 字节 保存 ebp 寄存器
mov ebp, esp ebp ebp 指向当前栈帧 设置新的栈帧
push ebx ebx, esp ebx 压入栈,esp 向下移动 4 字节 保存 ebx 寄存器
push ecx ecx, esp ecx 压入栈,esp 向下移动 4 字节 保存 ecx 寄存器
sub esp, 10h esp esp 向下移动 16 字节 分配空间给局部变量和调用栈
call __x86_get_pc_thunk_bx ebx ebx 存储程序地址 获取程序计数器(PC)地址
add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $) ebx ebx 更新为程序的地址 计算全局偏移表的地址
mov [ebp+var_10], 0Ah eax var_10 被设置为 0xA 0xA 存储到局部变量 var_10
mov [ebp+var_C], 10h eax var_C 被设置为 0x10 0x10 存储到局部变量 var_C
push [ebp+var_C] esp esp 向下移动 4 字节,压入 var_C 压栈 var_C 的值
push [ebp+var_10] esp esp 向下移动 4 字节,压入 var_10 压栈 var_10 的值
call add eax, ebx eax 存储 add 函数的返回值 调用 add 函数
add esp, 8 esp esp 向上移动 8 字节 恢复栈指针
sub esp, 8 esp esp 向下移动 8 字节 printf 调用分配空间
push eax eax, esp eax 保存返回值,esp 向下移动 4 字节 eax 中的返回值压栈
lea eax, (aD - 3FD8h)[ebx] eax, ebx eax 存储格式化字符串 %d\n 的地址 获取字符串 %d\n 的地址
push eax eax, esp esp 向下移动 4 字节,压入字符串地址 压栈格式化字符串
call _printf eax, esp eax 返回打印结果 调用 printf 函数打印结果
add esp, 10h esp esp 向上移动 16 字节 恢复栈指针
mov eax, 0 eax eax 被设置为 0 设置返回值为 0
lea esp, [ebp-8] esp esp 恢复为函数调用前的栈位置 恢复栈指针
pop ecx ecx, esp ecx 恢复,esp 向上移动 4 字节 恢复寄存器 ecx
pop ebx ebx, esp ebx 恢复,esp 向上移动 4 字节 恢复寄存器 ebx
pop ebp ebp, esp ebp 恢复,esp 向上移动 4 字节 恢复寄存器 ebp
lea esp, [ecx-4] ecx, esp esp 恢复到原始位置 恢复栈指针,退出函数
retn eax, ebx, ecx, esp 函数返回 返回并恢复执行流
mov edx, [ebp+arg_0] edx edx = arg_0 获取 add 函数的第一个参数
mov eax, [ebp+arg_4] eax eax = arg_4 获取 add 函数的第二个参数
add eax, edx eax, edx eax = eax + edx 执行加法操作
pop ebp ebp, esp ebp 恢复,esp 向上移动 4 字节 恢复栈帧
retn eax, ebx, ecx, esp 函数返回 返回并恢复执行流





Visual Studio vs. GCC 编译器对比

在这部分,你可以针对栈管理、寄存器使用、优化等方面,详细比较两者生成的汇编代码。
例如:

GCC 编译与 Visual Studio 编译的不同之处

汇编代码 影响的寄存器 寄存器值变化 注释
GCC 编译
main: 主程序入口
mov eax, [ebp+var_4] eax eax = var_4 var_4 加载到 eax 寄存器
call _printf 调用 printf 函数
add esp, 8 esp esp += 8 清理栈(为 printf 调用释放空间)
Visual Studio 编译
004118C0 push ebp ebp 保存 ebp 寄存器值,保护栈帧
004118C1 mov ebp, esp ebp ebp = esp 设置新的栈帧指针
004118C3 sub esp, 0D8h esp esp -= 0xD8 为局部变量分配空间
004118E0 call @__CheckForDebuggerJustMyCode@4 检查调试器的存在,增加调试保护
call add 调用 add 函数
call _printf 调用 printf 函数
不同点总结
Visual Studio 编译时多了调试保护函数 __CheckForDebuggerJustMyCode 用于检查调试器,防止调试攻击
Visual Studio 编译使用了更多的栈操作 ebp, esp 分配了更多的栈空间(sub esp, 0D8h)和保护机制 (推送 ebx, esi, edi)
GCC 更简洁 代码简洁,直接进行 movcall,没有额外的栈帧操作





总结






​ 通过这次汇编作业,我对汇编语言和低级编程有了更深的理解。尽管程序的本质功能简单——比如执行一个简单的加法操作和打印结果,但它的底层汇编代码却呈现出了一些复杂的行为和细节。这让我感触颇深,特别是在对比 GCC 和 Visual Studio 编译器生成的汇编代码时,明显感受到两者在处理程序时的不同。

参考资料






【大学生扫盲课】4 typora安装与配置 markdown语法说明_哔哩哔哩_bilibili

使用Hexo搭建个人博客手摸手教学(15)|使用picgo+github搭建免费个人图床_哔哩哔哩_bilibili

https://chatgpt.com