为何无标准库编译的MIPS32(R3000)简易C程序出现段错误?
尝试编写一个调用函数并将返回值作为退出码的MIPS架构C程序,代码如下:
int increment(int a) { return a + 1; } void __start() { // 调用increment函数并获取返回值 int exitcode = increment(1337); // 执行exit系统调用 asm volatile ( "li $v0, 0x1096\n" // MIPS的exit系统调用号(0x1096) "move $a0, %0\n" // 退出码 "syscall\n" // 触发系统调用 : : "r" (exitcode) ); // 此行永远不会被执行 while (1) {} }
使用以下命令编译:
/usr/bin/mips-linux-gnu-gcc -static -mno-mt -mfp32 -march=R3000 -EB -O0 -DNDEBUG -nostdlib main.c -o program
通过qemu-mips运行时触发段错误,调试输出如下:
$ qemu-mips -d in_asm,cpu program ---------------- IN: __start 0x0040015c: lui gp,0x2 0x00400160: addiu gp,gp,-32716 0x00400164: addu gp,gp,t9 0x00400168: addiu sp,sp,-32 0x0040016c: sw ra,28(sp) 0x00400170: sw gp,16(sp) 0x00400174: li a0,1337 0x00400178: lw t9,-32744(gp) 0x0040017c: nop 0x00400180: bal 0x400150 0x00400184: nop pc=0x0040015c HI=0x00000000 LO=0x00000000 ds 00a2 00000000 0 GPR00: r0 00000000 at 00000000 v0 00000000 v1 00000000 GPR04: a0 00000000 a1 00000000 a2 00000000 a3 00000000 GPR08: t0 00000000 t1 00000000 t2 00000000 t3 00000000 GPR12: t4 00000000 t5 00000000 t6 00000000 t7 00000000 GPR16: s0 00000000 s1 00000000 s2 00000000 s3 00000000 GPR20: s4 00000000 s5 00000000 s6 00000000 s7 00000000 GPR24: t8 00000000 t9 00000000 k0 00000000 k1 00000000 GPR28: gp 00000000 sp 407fff50 s8 00000000 ra 00000000 CP0 Status 0x20000010 Cause 0x00000000 EPC 0x00000000 Config0 0x80008482 Config1 0x9e190c8f LLAddr 0x0000000000000000 Config2 0x80000000 Config3 0x00000000 Config4 0x00000000 Config5 0x00000000 qemu: uncaught target signal 11 (Segmentation fault) - core dumped Segmentation fault (core dumped)
使用-O3编译时,increment函数会被编译器内联优化,程序可正常运行;但在-O0级别下,任何函数调用都会触发段错误,需排查原因。
核心问题是全局指针($gp)初始化无效,导致函数调用时通过$gp访问全局偏移表(GOT)触发非法内存访问。
在MIPS架构中,低优化级别(如-O0)下,编译器会依赖$gp寄存器来定位全局变量和函数的GOT条目,进而完成函数调用。但编译时使用了-nostdlib选项,跳过了标准库的启动代码——而标准库启动代码的关键职责之一就是正确初始化$gp,使其指向GOT的有效地址。
当前__start函数中的$gp初始化逻辑依赖t9寄存器的值,但程序启动时t9并未被正确设置,导致计算出的$gp地址完全无效。当执行到lw t9,-32744(gp)时,会访问非法内存地址,直接触发段错误。
而-O3优化时,编译器会将increment函数内联展开,无需通过GOT查找函数地址,自然绕开了$gp初始化失败的问题,因此程序能正常运行。
有两种可行的修复方式:
方式一:保留必要启动代码
将编译选项中的-nostdlib替换为-nostartfiles。-nostartfiles会跳过标准库中main函数的包装逻辑,但保留用于初始化$gp等关键寄存器的启动代码。修改后的编译命令:
/usr/bin/mips-linux-gnu-gcc -static -mno-mt -mfp32 -march=R3000 -EB -O0 -DNDEBUG -nostartfiles main.c -o program
启动代码会自动完成$gp的正确初始化,函数调用即可正常执行。
方式二:手动初始化$gp
如果必须使用-nostdlib,可以手动通过汇编代码正确初始化$gp。需要先确定二进制文件中GOT的绝对地址,然后直接将其加载到$gp中。例如:
void __start() { // 手动初始化gp为GOT的绝对地址(需根据实际二进制调整0x4000f0) asm volatile ("li $gp, 0x4000f0"); int exitcode = increment(1337); asm volatile ( "li $v0, 0x1096\n" "move $a0, %0\n" "syscall\n" : : "r" (exitcode) ); while (1) {} }
这种方式需要通过objdump等工具查看二进制的GOT地址,灵活性较差,仅适合特殊场景。
内容的提问来源于stack exchange,提问作者warchantua

