32位Lubuntu下汇编函数返回C程序的栈指针处理及段错误排查
段错误根因定位
32位Linux cdecl调用约定下,现有代码存在3个核心栈操作错误,直接导致程序在printf返回后栈布局被破坏,触发段错误:
- 入参获取逻辑错误:32位cdecl规范下函数入参通过栈传递,不存在入参存入eax的规则。printFunc的入参x存储在栈帧的
ebp+8位置,原代码用mov ebx, eax取入参属于逻辑错误,只是凑巧测试时eax值与入参一致,才没有暴露随机取值的问题。 - 跨函数调用后未清理栈参数:cdecl约定下所有函数参数由调用方负责清理。原代码在调用
c_checkValidity、printf时压入的参数,在call返回后从未手动回收栈空间,导致栈指针持续向低地址偏移,偏离正确位置。 - 栈恢复顺序错误:原代码在未将栈指针调整到
pushad执行后的位置时,就直接执行popad。这一步会将栈上残留的函数参数值当成之前保存的通用寄存器值弹出,直接篡改ebx、ebp等关键寄存器;后续执行mov esp, ebp时,会使用被篡改的非法ebp值设置栈指针,访问非法内存地址触发段错误。
这也是为什么程序可以正常完成打印流程——printf执行时栈上的参数布局是正确的,输出完成返回后栈布局才被破坏触发崩溃。
修复后可正常运行的汇编代码
section .text global printFunc extern c_checkValidity extern printf section .data fmt: db "%d", 10, 0 printFunc: push ebp mov ebp, esp pushad ; 保存所有通用寄存器,此时esp = ebp - 32 mov ebx, [ebp+8] ; 正确从栈帧取入参x,替代原错误的mov ebx, eax push ebx ; 为c_checkValidity压入入参 call c_checkValidity add esp, 4 ; 清理c_checkValidity的1个4字节参数,esp回到ebp-32位置 cmp eax, 1 je multby4 sal ebx, 3 ; 奇数乘8 jmp print_result multby4: sal ebx, 2 ; 偶数乘4 print_result: push ebx ; 为printf压入待打印值 push dword fmt ; 为printf压入格式串 call printf add esp, 8 ; 清理printf的2个4字节参数,esp回到ebp-32位置 popad ; 此时esp位置匹配pushad时的位置,正确恢复所有寄存器 pop ebp ret
32位x86跨语言汇编调用C程序的通用栈维护规则
- 栈布局对齐要求:栈向低地址生长,
call指令自动压入4字节返回地址,ret指令自动弹出4字节返回地址;任何不匹配的压栈、弹栈操作都会直接破坏栈布局。 - 参数清理责任:cdecl约定下,被调用函数不需要清理入参栈空间,所有压入的函数参数必须由调用方在call返回后,通过
add esp, N(N为压入的参数总字节数,32位下每个参数占4字节)回收,不能依赖被调用方做栈清理。 - 栈帧参考点规则:进入函数后通过
push ebp、mov ebp, esp建立固定栈帧后,入参固定在ebp+8(第一个入参)、ebp+12(第二个入参)等偏移位置,不受中间压栈操作的影响;函数返回前必须保证栈指针回到与栈帧匹配的位置,才能正确获取旧基址指针和返回地址。 - 寄存器保护规则:eax、ecx、edx属于调用方保存的寄存器,被调用函数可以随意修改;ebx、esi、edi、ebp属于被调用方保存的寄存器,被调用函数如果要使用这些寄存器,必须通过
push/pop或pushad/popad在函数返回前恢复原值,否则会破坏主调函数的寄存器上下文。 - 返回值传递规则:32位C程序的函数返回值统一存入eax寄存器,哪怕函数声明为void类型,主动修改eax的值不会影响主调函数的执行逻辑。
内容的提问来源于stack exchange,提问作者Veritable
相关产品推荐
相关产品推荐

