调用Glibc函数未执行xor %rax, %rax导致崩溃的原因及RIP寻址传参疑问
关于Glibc printf调用的两个问题解答
嘿,这俩问题都是x86-64 System V调用约定和位置无关代码里的经典坑,我来给你讲明白:
问题1:为何部分场景下未执行xor %rax, %rax调用printf会崩溃,而部分场景不会?
在x86-64的System V AMD64调用约定里,可变参数函数(比如printf)要求调用者把%rax寄存器设置为通过XMM寄存器传递的浮点参数数量。printf是典型的可变参数函数,必须严格遵守这个规则。
当你没写xor %rax, %rax时,%rax里留的是之前程序执行后剩下的垃圾值:
- 如果这个垃圾值刚好是0,那完全符合要求(毕竟你的printf调用里没有浮点参数),程序自然能正常跑,不会崩溃;
- 如果垃圾值大于0,printf会误以为你传了对应数量的浮点参数,会尝试去读取XMM0到XMM(rax-1)里的值,甚至会根据这个数量调整栈布局。但实际上你根本没传这些参数,这就会导致栈不平衡、非法内存访问或者破坏后续函数的运行上下文,最终触发崩溃。
至于“部分场景没事”,纯粹是运气——某次运行时%rax里的垃圾值刚好是0,或者垃圾值对应的操作没触发致命错误(比如某些旧版libc对这种情况有容错,但绝对不能依赖)。但不管怎样,显式把%rax清零才是正确的写法,不能靠碰运气。
问题2:为何使用lea some_string(%rip), %rdi,这种传参方式与常规方式有何不同?
这是**位置无关代码(PIC/PIE)**的标准写法,和直接mov $some_string, %rdi的核心区别在寻址逻辑:
mov $some_string, %rdi是绝对寻址:链接器会把some_string的绝对内存地址直接编码到指令里。这种写法只能在非位置无关的可执行文件(非PIE)里正常工作——因为程序加载到内存的地址是链接时固定死的。如果是动态链接库或者PIE可执行文件(现在很多系统默认开启PIE),程序会被加载到随机的内存地址,这个硬编码的绝对地址就失效了,程序根本找不到字符串的位置。lea some_string(%rip), %rdi是RIP相对寻址:RIP寄存器存的是当前指令的下一条指令的地址,lea指令会计算当前RIP地址加上some_string相对于当前指令的偏移量,得到字符串的实际内存地址。不管程序被加载到内存的哪个位置,这个偏移量都是固定的,所以总能正确找到字符串。这种写法是动态链接库和PIE可执行文件的必备写法,保证代码在不同加载地址下都能正常运行。
另外,RIP相对寻址还有个小好处:生成的指令长度更短(64位模式下,绝对寻址的mov可能需要8字节立即数,而RIP相对的lea只需要4字节偏移),能省一点代码空间。
内容的提问来源于stack exchange,提问作者xperious
相关产品推荐
相关产品推荐

