gcc arm 在系统调用之前优化掉参数
gcc arm optimizes away parameters before System Call
我正在尝试使用 gcc arm 在 arm7tdmi-s 上实现一些 "OSEK-Services"。不幸的是,调高优化级别会导致 "wrong" 代码生成。我不明白的主要是编译器似乎忽略了过程调用标准,例如通过将参数移动到寄存器 r0-r3 来将参数传递给函数。我知道可以内联函数调用,但参数仍然需要在寄存器中才能执行系统调用。
考虑以下代码来演示我的问题:
unsigned SysCall(unsigned param)
{
volatile unsigned ret_val;
__asm __volatile
(
"swi 0 \n\t" /* perform SystemCall */
"mov %[v], r0 \n\t" /* move the result into ret_val */
: [v]"=r"(ret_val)
:: "r0"
);
return ret_val; /* return the result */
}
int main()
{
unsigned retCode;
retCode = SysCall(5); // expect retCode to be 6 when returning back to usermode
}
我在汇编中编写了顶级软件中断处理程序如下:
.type SWIHandler, %function
.global SWIHandler
SWIHandler:
stmfd sp! , {r0-r2, lr} @save regs
ldr r0 , [lr, #-4] @load sysCall instruction and extract sysCall number
bic r0 , #0xff000000
ldr r3 , =DispatchTable @load dispatchTable
ldr r3 , [r3, r0, LSL #2] @load sysCall address into r3
ldmia sp, {r0-r2} @load parameters into r0-r2
mov lr, pc
bx r3
stmia sp ,{r0-r2} @store the result back on the stack
ldr lr, [sp, #12] @restore return address
ldmfd sp! , {r0-r2, lr} @load result into register
movs pc , lr @back to next instruction after swi 0
调度 table 看起来像这样:
DispatchTable:
.word activateTaskService
.word getTaskStateService
SystemCall 函数如下所示:
unsigned activateTaskService(unsigned tID)
{
return tID + 1; /* only for demonstration */
}
运行 在没有优化的情况下一切正常,参数在寄存器中是预期的:
请参阅以下带有 -O0 优化的代码:
00000424 <main>:
424: e92d4800 push {fp, lr}
428: e28db004 add fp, sp, #4
42c: e24dd008 sub sp, sp, #8
430: e3a00005 mov r0, #5 @move param into r0
434: ebffffe1 bl 3c0 <SysCall>
000003c0 <SysCall>:
3c0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
3c4: e28db000 add fp, sp, #0
3c8: e24dd014 sub sp, sp, #20
3cc: e50b0010 str r0, [fp, #-16]
3d0: ef000000 svc 0x00000000
3d4: e1a02000 mov r2, r0
3d8: e50b2008 str r2, [fp, #-8]
3dc: e51b3008 ldr r3, [fp, #-8]
3e0: e1a00003 mov r0, r3
3e4: e24bd000 sub sp, fp, #0
3e8: e49db004 pop {fp} ; (ldr fp, [sp], #4)
3ec: e12fff1e bx lr
使用 -O3 编译相同的代码会产生以下汇编代码:
00000778 <main>:
778: e24dd008 sub sp, sp, #8
77c: ef000000 svc 0x00000000 @Inline SystemCall without passing params into r0
780: e1a02000 mov r2, r0
784: e3a00000 mov r0, #0
788: e58d2004 str r2, [sp, #4]
78c: e59d3004 ldr r3, [sp, #4]
790: e28dd008 add sp, sp, #8
794: e12fff1e bx lr
注意 systemCall 如何在没有分配值 5 t0 r0 的情况下被内联。
我的第一种方法是通过调整上面的 SysCall 函数将这些值手动移动到寄存器中,如下所示:
unsigned SysCall(volatile unsigned p1)
{
volatile unsigned ret_val;
__asm __volatile
(
"mov r0, %[p1] \n\t"
"swi 0 \n\t"
"mov %[v], r0 \n\t"
: [v]"=r"(ret_val)
: [p1]"r"(p1)
: "r0"
);
return ret_val;
}
它似乎适用于这个最小的示例,但我不太确定这是否是最佳做法。为什么编译器认为他在内联函数时可以省略参数呢?有人对这种方法是否可行或应该采取哪些不同的做法有任何建议吗?
提前致谢
C源代码中的函数调用没有指示编译器根据ABI调用函数。它指示编译器按照 C 标准中的模型调用函数,这意味着编译器必须以其选择的方式将参数传递给函数,并以相同的方式执行函数 C 标准中定义的可观察效果。
那些可观察到的效果不包括设置任何处理器寄存器。当 C 编译器内联函数时,不需要设置任何特定的处理器寄存器。如果它调用一个使用 ABI 的函数来进行外部调用,那么它就必须设置寄存器。内联调用不需要遵守 ABI。
因此仅仅将您的系统请求放入由 C 源代码构建的函数中并不能保证任何寄存器都将被设置。
对于 ARM,您应该做的是定义分配给所需寄存器的寄存器变量,并将它们用作汇编指令的输入和输出:
unsigned SysCall(unsigned param)
{
register unsigned Parameter __asm__("r0") = param;
register unsigned Result __asm__("r0");
__asm__ volatile
(
"swi 0"
: "=r" (Result)
: "r" (Parameter)
: // "memory" // if any inputs are pointers
);
return Result;
}
(这是 GCC 的一个主要问题;它很难看,并且 the documentation is poor. But see also https://whosebug.com/tags/inline-assembly/info 对于某些链接。对于某些 ISA,GCC 具有方便的特定寄存器约束,您可以使用它来代替 r
,但是不适用于 ARM。)寄存器变量不需要是易失性的;编译器知道它们将用作汇编指令的输入和输出。
asm
语句本身应该是 volatile
如果它除了产生 return 值之外还有副作用。 (例如 getpid()
不需要是 volatile
。)
一个非volatile
asm
语句如果输出未被使用可以被优化掉,或者如果它与相同的输入一起使用(就像一个纯函数调用)可以被提升到循环之外.这几乎不是您想要的系统调用。
如果任何输入是指向内核将读取或修改的内存的指针,您还需要一个 "memory"
破坏器。有关详细信息,请参阅 (以及使用虚拟内存输入或输出以避免 "memory"
破坏的方法。)
"memory"
破坏 mmap/munmap 或其他影响内存含义的系统调用也是明智的;您不希望编译器决定在 munmap
之后而不是之前进行存储。
我正在尝试使用 gcc arm 在 arm7tdmi-s 上实现一些 "OSEK-Services"。不幸的是,调高优化级别会导致 "wrong" 代码生成。我不明白的主要是编译器似乎忽略了过程调用标准,例如通过将参数移动到寄存器 r0-r3 来将参数传递给函数。我知道可以内联函数调用,但参数仍然需要在寄存器中才能执行系统调用。
考虑以下代码来演示我的问题:
unsigned SysCall(unsigned param)
{
volatile unsigned ret_val;
__asm __volatile
(
"swi 0 \n\t" /* perform SystemCall */
"mov %[v], r0 \n\t" /* move the result into ret_val */
: [v]"=r"(ret_val)
:: "r0"
);
return ret_val; /* return the result */
}
int main()
{
unsigned retCode;
retCode = SysCall(5); // expect retCode to be 6 when returning back to usermode
}
我在汇编中编写了顶级软件中断处理程序如下:
.type SWIHandler, %function
.global SWIHandler
SWIHandler:
stmfd sp! , {r0-r2, lr} @save regs
ldr r0 , [lr, #-4] @load sysCall instruction and extract sysCall number
bic r0 , #0xff000000
ldr r3 , =DispatchTable @load dispatchTable
ldr r3 , [r3, r0, LSL #2] @load sysCall address into r3
ldmia sp, {r0-r2} @load parameters into r0-r2
mov lr, pc
bx r3
stmia sp ,{r0-r2} @store the result back on the stack
ldr lr, [sp, #12] @restore return address
ldmfd sp! , {r0-r2, lr} @load result into register
movs pc , lr @back to next instruction after swi 0
调度 table 看起来像这样:
DispatchTable:
.word activateTaskService
.word getTaskStateService
SystemCall 函数如下所示:
unsigned activateTaskService(unsigned tID)
{
return tID + 1; /* only for demonstration */
}
运行 在没有优化的情况下一切正常,参数在寄存器中是预期的: 请参阅以下带有 -O0 优化的代码:
00000424 <main>:
424: e92d4800 push {fp, lr}
428: e28db004 add fp, sp, #4
42c: e24dd008 sub sp, sp, #8
430: e3a00005 mov r0, #5 @move param into r0
434: ebffffe1 bl 3c0 <SysCall>
000003c0 <SysCall>:
3c0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
3c4: e28db000 add fp, sp, #0
3c8: e24dd014 sub sp, sp, #20
3cc: e50b0010 str r0, [fp, #-16]
3d0: ef000000 svc 0x00000000
3d4: e1a02000 mov r2, r0
3d8: e50b2008 str r2, [fp, #-8]
3dc: e51b3008 ldr r3, [fp, #-8]
3e0: e1a00003 mov r0, r3
3e4: e24bd000 sub sp, fp, #0
3e8: e49db004 pop {fp} ; (ldr fp, [sp], #4)
3ec: e12fff1e bx lr
使用 -O3 编译相同的代码会产生以下汇编代码:
00000778 <main>:
778: e24dd008 sub sp, sp, #8
77c: ef000000 svc 0x00000000 @Inline SystemCall without passing params into r0
780: e1a02000 mov r2, r0
784: e3a00000 mov r0, #0
788: e58d2004 str r2, [sp, #4]
78c: e59d3004 ldr r3, [sp, #4]
790: e28dd008 add sp, sp, #8
794: e12fff1e bx lr
注意 systemCall 如何在没有分配值 5 t0 r0 的情况下被内联。
我的第一种方法是通过调整上面的 SysCall 函数将这些值手动移动到寄存器中,如下所示:
unsigned SysCall(volatile unsigned p1)
{
volatile unsigned ret_val;
__asm __volatile
(
"mov r0, %[p1] \n\t"
"swi 0 \n\t"
"mov %[v], r0 \n\t"
: [v]"=r"(ret_val)
: [p1]"r"(p1)
: "r0"
);
return ret_val;
}
它似乎适用于这个最小的示例,但我不太确定这是否是最佳做法。为什么编译器认为他在内联函数时可以省略参数呢?有人对这种方法是否可行或应该采取哪些不同的做法有任何建议吗?
提前致谢
C源代码中的函数调用没有指示编译器根据ABI调用函数。它指示编译器按照 C 标准中的模型调用函数,这意味着编译器必须以其选择的方式将参数传递给函数,并以相同的方式执行函数 C 标准中定义的可观察效果。
那些可观察到的效果不包括设置任何处理器寄存器。当 C 编译器内联函数时,不需要设置任何特定的处理器寄存器。如果它调用一个使用 ABI 的函数来进行外部调用,那么它就必须设置寄存器。内联调用不需要遵守 ABI。
因此仅仅将您的系统请求放入由 C 源代码构建的函数中并不能保证任何寄存器都将被设置。
对于 ARM,您应该做的是定义分配给所需寄存器的寄存器变量,并将它们用作汇编指令的输入和输出:
unsigned SysCall(unsigned param)
{
register unsigned Parameter __asm__("r0") = param;
register unsigned Result __asm__("r0");
__asm__ volatile
(
"swi 0"
: "=r" (Result)
: "r" (Parameter)
: // "memory" // if any inputs are pointers
);
return Result;
}
(这是 GCC 的一个主要问题;它很难看,并且 the documentation is poor. But see also https://whosebug.com/tags/inline-assembly/info 对于某些链接。对于某些 ISA,GCC 具有方便的特定寄存器约束,您可以使用它来代替 r
,但是不适用于 ARM。)寄存器变量不需要是易失性的;编译器知道它们将用作汇编指令的输入和输出。
asm
语句本身应该是 volatile
如果它除了产生 return 值之外还有副作用。 (例如 getpid()
不需要是 volatile
。)
一个非volatile
asm
语句如果输出未被使用可以被优化掉,或者如果它与相同的输入一起使用(就像一个纯函数调用)可以被提升到循环之外.这几乎不是您想要的系统调用。
如果任何输入是指向内核将读取或修改的内存的指针,您还需要一个 "memory"
破坏器。有关详细信息,请参阅 "memory"
破坏的方法。)
"memory"
破坏 mmap/munmap 或其他影响内存含义的系统调用也是明智的;您不希望编译器决定在 munmap
之后而不是之前进行存储。