汇编作业报告
Last Update:
汇编作业报告
[TOC]
引言
本报告旨在分析一个简单的 C 语言程序,分别在 Visual Studio 和 GCC 环境下编译生成的汇编代码。我们将通过对比两者的汇编指令,深入探讨编译器生成的优化和差异。重点分析栈操作、寄存器的变化以及函数调用机制等方面。
作业说明
分析的 C 语言源码见 2,是简单的 add 函数和 printf 输出 ,我会直接分析 visual stdio 中的汇编代码(一些 visual stdio 特有的汇编操作会进行标出),之后也会和我用 ubuntu 中的 gcc 编译然后 ida 查看的汇编代码进行比较。
C 程序源码
1 |
|
每条语句对寄存器的影响情况
visual stdio 版
main 函数汇编代码分析
1 |
|
汇编 | 影响 | 受影响的寄存器取值 | 注释 |
---|---|---|---|
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 |
|
汇编 | 影响 | 受影响的寄存器取值 | 注释 |
---|---|---|---|
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 |
|
汇编 | 影响 | 受影响的寄存器取值 | 注释 |
---|---|---|---|
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 |
|
汇编 | 影响 | 受影响的寄存器取值 | 注释 |
---|---|---|---|
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 更简洁 | 代码简洁,直接进行 mov 和 call ,没有额外的栈帧操作 |
总结
通过这次汇编作业,我对汇编语言和低级编程有了更深的理解。尽管程序的本质功能简单——比如执行一个简单的加法操作和打印结果,但它的底层汇编代码却呈现出了一些复杂的行为和细节。这让我感触颇深,特别是在对比 GCC 和 Visual Studio 编译器生成的汇编代码时,明显感受到两者在处理程序时的不同。
参考资料
【大学生扫盲课】4 typora安装与配置 markdown语法说明_哔哩哔哩_bilibili