x86_64汇编栈原理、对齐规则及Segmentation Fault段错误排查求助
让我们一步步拆解你的问题,先搞定栈对齐的核心规则,再修正你代码里的关键错误——这些错误才是导致段错误的根本原因。
首先明确x86_64的栈对齐硬性要求
在Linux的x86_64 System V ABI规范中,调用任何函数(比如printf)之前,栈指针RSP必须是16字节的整数倍。原因很简单:call指令会把8字节的返回地址压入栈,调用函数后RSP会变成16n - 8(即8字节偏移,未对齐);进入函数后执行push %rbp; mov %rsp, %rbp,这两步会让RSP再减8,回到16n - 16,重新恢复16字节对齐状态。
如果后续要在栈上分配空间,必须用16字节倍数的数值(比如sub $0x10, %rsp、sub $0x20, %rsp),全程保持栈对齐。
你的代码里的3个致命错误
1. 硬编码的RIP相对偏移
你写的lea 0xe99(%rip), %rax是绝对错误的——这个硬编码的偏移是你编译某次代码时的临时值,换个环境或者重新编译就会指向完全错误的内存地址,直接触发段错误。正确的做法是用符号名来寻址:lea print_format(%rip), %rdi,这是x86_64访问全局变量的标准方式,编译器会自动计算正确的RIP相对偏移。
2. 重复的栈恢复指令
你最后写了pop %rbp; leave; ret,但leave指令本身就等价于mov %rbp, %rsp; pop %rbp——这就导致你重复执行了pop %rbp,栈指针直接乱掉,ret时找不到正确的返回地址,必然触发段错误。
3. 不必要的栈存储(非致命,但增加出错概率)
64位平台有足够多的通用寄存器,完全没必要把计数器和结果存在栈上——栈操作不仅效率低,还容易因为地址计算错误出问题。
修正后的最简代码(复用寄存器逻辑)
直接沿用你原32位代码的寄存器思路,适配64位规则即可:
.data print_format: .string "%d\n" .text .global main main: # 建立栈帧,此时RSP已恢复16字节对齐 push %rbp mov %rsp, %rbp # 用寄存器存储结果和计数器,无需栈空间 movl $0, %eax # eax = 求和结果,初始为0 movl $10, %ecx # ecx = 循环计数器,初始为10 sum: addl %ecx, %eax # eax += ecx loop sum # 32位loop指令在64位下依然可用,自动递减ecx并判断跳转 # 准备调用printf,此时RSP保持16字节对齐,满足ABI要求 movl %eax, %esi # printf第二个参数:要打印的数值 lea print_format(%rip), %rdi # printf第一个参数:格式字符串地址 movl $0, %eax # 告诉printf没有浮点参数(必须设置) call printf # 恢复栈帧并返回 movl $0, %eax # main函数返回值为0 leave # 等价于mov %rbp, %rsp; pop %rbp ret
如果一定要练习栈存储(修正版)
如果是为了练习栈操作,下面是修正后的栈存储版本,全程保证栈对齐:
.data print_format: .string "%d\n" .text .global main main: push %rbp mov %rsp, %rbp sub $0x10, %rsp # 分配16字节栈空间,保持栈对齐 movl $0, -0x4(%rbp) # 栈上-0x4位置存求和结果,初始0 movl $10, -0x8(%rbp) # 栈上-0x8位置存计数器,初始10 sum: movl -0x8(%rbp), %eax addl %eax, -0x4(%rbp) # 结果 += 计数器值 subl $1, -0x8(%rbp) # 计数器减1 cmpl $0, -0x8(%rbp) jne sum # 调用printf,此时RSP为%rbp-0x10,依然是16字节对齐 movl -0x4(%rbp), %esi lea print_format(%rip), %rdi movl $0, %eax call printf movl $0, %eax leave ret
总结避坑要点
- 调用任何函数前,务必确保
RSP是16字节的整数倍; - 永远不要硬编码内存偏移,用符号名配合RIP相对寻址;
- 恢复栈帧时只用
leave+ret,不要重复执行pop %rbp; - 分配栈空间时,只使用16字节倍数的数值。
内容的提问来源于stack exchange,提问作者Unix_Gray

