【问题标题】:gcc arm optimizes away parameters before System Callgcc arm 在系统调用之前优化了参数
【发布时间】:2020-07-09 21:00:29
【问题描述】:

我正在尝试使用 gcc arm 在 arm7tdmi-s 上实现一些“OSEK 服务”。不幸的是,调高优化级别会导致“错误”的代码生成。我不明白的主要事情是编译器似乎忽略了过程调用标准,例如通过将参数移动到寄存器 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

调度表如下所示:

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;
}

它似乎在这个最小的例子中有效,但我不太确定这是否是最佳做法。为什么编译器认为他可以在内联函数时省略参数?有人对这种方法是否可行或应该采取哪些不同的做法提出任何建议?

提前谢谢你

【问题讨论】:

  • 在执行asm 语句时,您不能假设除了您在输入和输出约束中指定的寄存器之外的任何寄存器都保存任何特定值。您所做的显然是滥用内联汇编。
  • 详情请read the manual
  • 所以要解决您的问题:提供明确的输入和输出约束,指示在执行asm 语句时哪些寄存器必须保存哪些变量的值。我认为最简单的方法是将register variablesr 约束结合使用。
  • @EricPostpischil 我链接的关于寄存器变量的手册特别提到,唯一受支持的用途是为asm 语句指定操作数,其粒度比机器的约束允许的粒度更细。据我所知,指定特定寄存器没有 ARM 约束,因此必须使用寄存器变量。
  • 对于这种微不足道的事情,为什么不使用真正的汇编,编译器不会妨碍您,而是帮助您。

标签: c assembly arm system-calls inline-assembly


【解决方案1】:

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 很差。但另请参阅 https://stackoverflow.com/tags/inline-assembly/info 以获取一些链接。对于某些 ISA,GCC 具有方便的特定寄存器约束,您可以使用而不是 @987654325 @,但不适用于 ARM。)寄存器变量不需要是易失的;编译器知道它们将用作汇编指令的输入和输出。

asm 语句本身应该是volatile,如果它有副作用而不是产生返回值。 (例如getpid() 不需要是volatile。)

如果输出未使用,则可以优化带有输出的非volatile asm 语句,或者如果它与相同的输入一起使用(如纯函数调用),则可以将其提升出循环。这几乎不是您想要的系统调用。

如果任何输入是指向内核将读取或修改的内存的指针,您还需要一个"memory" clobber。有关更多详细信息,请参阅How can I indicate that the memory *pointed* to by an inline ASM argument may be used?(以及使用虚拟内存输入或输出来避免"memory" 破坏的方法。)

在 mmap/munmap 或其他影响内存含义的系统调用上使用 "memory" 破坏也是明智的;您不希望编译器决定在 munmap 之后而不是之前进行存储。

【讨论】:

  • 非常感谢,看来可以了!从来没有想过使用 register 关键字...
  • @UltraGeralt:你几乎肯定想要asm volatile;请参阅我对此答案的编辑。除非系统调用是getpid,或者如果返回值未使用,则可以优化掉,不像write()
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-13
  • 2019-05-18
  • 1970-01-01
  • 1970-01-01
  • 2022-01-07
  • 2022-01-15
  • 1970-01-01
相关资源
最近更新 更多