ARM GCC内联汇编模板代码解析及相关技术疑问咨询
原始内联汇编代码(Zephyr项目)
static inline uintptr_t arch_syscall_invoke3(uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t call_id) { register uint32_t ret __asm__("r0") = arg1; register uint32_t r1 __asm__("r1") = arg2; register uint32_t r2 __asm__("r2") = arg3; register uint32_t r6 __asm__("r6") = call_id; __asm__ volatile("svc %[svid]\n" : "=r"(ret), "=r"(r1), "=r"(r2) // HERE 1 : [svid] "i" (_SVC_CALL_SYSTEM_CALL), // HERE 2 "r" (ret), "r" (r1), "r" (r2), "r" (r6) : "r8", "memory", "r3", "ip"); return ret; }
Godbolt生成的最终汇编代码
push {r6, r7, r8} ; ------------- A ------------- sub sp, sp, #20 add r7, sp, #0 str r0, [r7, #12] str r1, [r7, #8] str r2, [r7, #4] str r3, [r7] ldr r0, [r7, #12] ldr r1, [r7, #8] ldr r2, [r7, #4] ldr r6, [r7] svc #3 ; ------------- B ------------- mov r3, r0 ; ------------- C1 ------------- mov r0, r3 ; ------------- C2 ------------- adds r7, r7, #20 mov sp, r7 pop {r6, r7, r8} bx lr
从A到B的代码用于确保输入参数位于目标寄存器,符合系统调用约定。以下是针对三点技术疑问的解答:
疑问1
根据GCC扩展汇编文档,HERE1为OutputOperands(输出操作数),代表被汇编模板指令修改的C变量,是否意味着对应的系统调用函数会修改ret/r0、r1、r2寄存器?
解答:
没错,这里把ret(对应r0)、r1、r2声明为输出操作数,就是明确告诉GCC:执行svc指令后,这三个寄存器的值会被系统调用的内核处理逻辑改写。在ARM的系统调用约定中,除了r0用来返回主结果,r1和r2可能会携带额外的返回数据,或者被内核侧的处理过程修改。声明成输出操作数,能让GCC知道这些寄存器的值在汇编块执行后不能再被当作原来的输入值使用,避免优化时出现错误。
疑问2
HERE2为InputOperands(输入操作数),SVC指令仅需一个立即数参数,但此处指定了ret、r1、r2、r6四个输入操作数,为何需要声明这些?若仅在SVC前准备寄存器但不将其声明为输入操作数,会产生错误吗?
解答:
这是为了约束GCC的寄存器分配和优化行为:
- 我们用
register ... __asm__("rX")指定了变量绑定的寄存器,但如果不在输入操作数里声明,GCC可能会在汇编块执行前,把这些寄存器里的值挪去别的地方(比如为了腾出寄存器给其他操作),导致svc执行时寄存器里的参数不对。 - 声明成输入操作数,相当于告诉GCC:这些寄存器里的值是汇编块需要用到的输入,不能随便改动。
如果不声明这些输入操作数,很大概率会出问题——GCC的优化器可能会认为这些寄存器的值在汇编块里没被用到(因为没声明输入),提前把它们的内容覆盖或者存到栈上,导致svc调用时参数完全错误,系统调用执行失败。
疑问3
生成的汇编代码中,C1(mov r3, r0)和C2(mov r0, r3)看似完全冗余,其存在的意义是什么?
解答:
这是因为我们在输出操作数里声明了ret(r0)、r1、r2为输出,但GCC可能为了满足输出操作数的"被修改"语义,生成了这些冗余指令。
具体来说,GCC看到输出操作数里有"=r"(ret),会认为r0的值必须被汇编块修改,但svc指令本身是隐式修改寄存器的,GCC没法直接追踪到这一点,所以会生成一个看似无用的赋值操作,来"标记"r0的值已经被更新。另外,也可能是因为我们把r0同时作为输入和输出操作数(输入里有"r"(ret),输出里有"=r"(ret)),GCC为了处理这种"读-改-写"的依赖关系,生成了这些冗余指令。
这种冗余指令在优化等级较低的时候容易出现,要是把编译优化等级调到-O2或者更高,这些冗余的mov指令大概率会被GCC优化掉。
内容的提问来源于stack exchange,提问作者smwikipedia

