Mik*_*vey 2 c optimization gcc arm inline-assembly
GCC 的最新版本(包括版本 12)实现了过程间分析,该分析会严重损坏 ARM/Thumb 上系统调用存根的以下(仅限 GCC)代码。
typedef struct { int sender; int arg; } message;
#define syscall(op) asm volatile ("svc %0" :: "i"(op))
#define SYS_SEND 9
#define NOINLINE __attribute((noinline))
void NOINLINE send(int dest, int type, message *msg)
{
syscall(SYS_SEND);
}
void send_int(int d, int t, int v)
{
message msg;
msg.arg = v;
send(d, t, &msg);
}
Run Code Online (Sandbox Code Playgroud)
目的是操作系统的陷阱处理程序将send通过访问参数寄存器的保存值来找到三个参数r0——r2在陷阱的异常帧中。问题显然是优化器在查看 的主体时send认为其消息参数的字段未被使用。msg.arg因此,主体中对 的赋值send_int被删除。这是通过编译上述源代码揭示的
arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O -g -Wall -ffreestanding -c trial.c
Run Code Online (Sandbox Code Playgroud)
并转储生成的目标代码。
为了禁用这种过度热情的优化,什么注释是合适的?
提供send附加属性noipa是可行的,但我对此感到紧张,因为它看起来特别不标准,并且 GCC 文档将其描述为主要用于编译器调试。noinline为了不理解的早期 GCC 版本,应保留现有属性noipa。GCC 会忽略未实现的属性,不是吗?
它还可以将msg缓冲区标记为send_int易失性(并通过在调用中添加强制转换来抑制警告send)。但这似乎特别晦涩难懂。
在指令上添加"memory"作为破坏者svc不起作用。
如果我像在支持汇编语言例程的单独文件中编写系统调用存根一样,它也可以解决问题,send这可能是最好、最可靠的方法。
我认为这个问题更深层次。此代码中的基本假设是,执行asm中的语句后send(),相关寄存器的内容仍与进入函数时一样。GCC 没有做出这样的承诺,也从未这样做过。编译器始终可以自由地在语句内任意位置修改寄存器,从asm概念上讲,这包括左大括号和语句之间的“空代码” asm。
当然,当不执行 IPA 时,编译器不会碰巧利用这种自由,因为没有充分的理由这样做,但它可以这样做。如果在过去几年的任何时候,编译器mov r0, #42在您的指令之前任意插入svc,这种行为将是愚蠢且低效的,但绝不是不正确或违反文档的。
如果您需要编译器在执行时确保某些值位于某些寄存器中asm,那么您必须通过指定适当的输入操作数明确地告诉它。这是GCC内联汇编的基本前提。由于您asm("svc %0")没有寄存器输入操作数,因此您实际上是在告诉编译器您不关心任何寄存器中的内容,并且它会相信您的话。
由于事实上您确实dest关心、type和的值message在寄存器中,因此您需要为每个值提供一个输入操作数。此外,由于您希望它们位于特定的寄存器中,因此您可以通过register asm("...")声明来实现这一点。请仔细注意,无论它看起来如何,这并不告诉GCC通常为变量保留特定的寄存器;当执行 asm 时,该值将位于正确的寄存器中,但不一定在任何其他时间。
其他注意事项:
memory除了这一切之外,你还需要破坏者。如果没有它,编译器将假定asm不会在内存中读取或写入任何对象。因此,它仍然可以确保您msg在寄存器中获得缓冲区指针,但是填充缓冲区的前面的存储可能会被优化为死亡。
(有更微妙的方法可以通知编译器 asm 从缓冲区读取msg,而不是从内存中的其他任何地方读取,请参阅How can I indicates that the memory *pointed* to by an inline ASM argument may be use?,但它们是特别微妙且难以正确。)
如果您的系统调用修改任何寄存器,包括用于输入的寄存器以及 CPSR 中的条件代码标志,那么您需要为每个寄存器包含破坏者或输出操作数。如果它修改任何输入寄存器,例如在 中保留返回值r0,那么您将列为dest读写操作数(约束),并在 asm 之后"+r"检索返回值(如果您关心它)。dest
在 GCC 内联汇编中有一个精心构建的 ARM 系统调用示例 at ARM 内联汇编:退出系统调用并从内存中读取值(noreturn当然,这些内容不适用于您;与该示例不同,您将必须支付注意输出和破坏)。
一旦完成,所有的noinline/东西都将是不必要的,因为即使noipa函数被内联或 IPA编译器也将保证正确填充寄存器,并且您将获得避免函数调用开销的好处。
现在,如果您不想遇到这些麻烦,那么可以,您可以send在纯 asm.s文件中编写该函数。然后它对编译器来说是不透明的,编译器在调用它时只需遵循通常的调用约定,并根据需要填充寄存器。您还可以自由地读写内存、修改任何被调用破坏的寄存器等等。
如果您想将其保留在.c文件中,可以使用naked属性,有效地告诉编译器简单地发出标签,然后传递asm语句的内容。这也将起到阻止内联和 IPA 的作用,因为整个函数对编译器来说变得不透明。
严格来说,naked仅支持基本asm(没有输入或输出操作数),因此从技术上讲,i系统调用号的操作数是不允许的。不过,我的猜测是它会毫无问题地工作。
就像一般性评论一样:我倾向于发现“如何禁用这种破坏我的代码的优化?” 几乎总是一个XY 问题。通常,问题实际上是程序员没有向编译器提供有关代码预期语义的正确信息,而编译器需要这些信息才能正确确定哪些优化是合法的。例外情况是,所讨论的优化实际上是编译器错误,禁用它可以作为解决方法,但这种情况很少见。
| 归档时间: |
|
| 查看次数: |
124 次 |
| 最近记录: |