写点什么

gcc 属性 __attribute__((naked)) 使用场景

作者:SkyFire
  • 2023-02-03
    陕西
  • 本文字数:2388 字

    阅读完需:约 8 分钟

问题背景

去年一时兴起,实现了一个协程库 cocpp:


在进行协程上下文的切换时,使用了 gcc 内嵌汇编来实现,第一版大概是这样:


void switch_to(co_byte **curr, co_byte **next){
__asm volatile("" :: : "memory");
__asm volatile("popq %rbp");
__asm volatile("movq %r8, 0(%rdi)"); __asm volatile("movq %r9, 8(%rdi)"); __asm volatile("movq %r10, 16(%rdi)"); __asm volatile("movq %r11, 24(%rdi)"); __asm volatile("movq %r12, 32(%rdi)"); __asm volatile("movq %r13, 40(%rdi)"); __asm volatile("movq %r14, 48(%rdi)"); __asm volatile("movq %r15, 56(%rdi)"); __asm volatile("movq %rdi, 64(%rdi)"); __asm volatile("movq %rsi, 72(%rdi)"); __asm volatile("movq %rbp, 80(%rdi)"); __asm volatile("movq %rbx, 88(%rdi)"); __asm volatile("movq %rdx, 96(%rdi)"); __asm volatile("movq %rax, 104(%rdi)"); __asm volatile("movq %rcx, 112(%rdi)"); __asm volatile("popq 128(%rdi)"); __asm volatile("pushf"); __asm volatile("popq 136(%rdi)"); __asm volatile("movq %rsp, 120(%rdi)"); // rsp必须在rip后保存,先恢复 ///////////////////////////////////////////////// __asm volatile("movq 120(%rsi), %rsp"); __asm volatile("pushq 136(%rsi)"); __asm volatile("popf"); __asm volatile("pushq 128(%rsi)"); __asm volatile("movq 112(%rsi), %rcx"); __asm volatile("movq 104(%rsi), %rax"); __asm volatile("movq 96(%rsi), %rdx"); __asm volatile("movq 88(%rsi), %rbx"); __asm volatile("movq 80(%rsi), %rbp"); __asm volatile("movq 64(%rsi), %rdi"); __asm volatile("movq 56(%rsi), %r15"); __asm volatile("movq 48(%rsi), %r14"); __asm volatile("movq 40(%rsi), %r13"); __asm volatile("movq 32(%rsi), %r12"); __asm volatile("movq 24(%rsi), %r11"); __asm volatile("movq 16(%rsi), %r10"); __asm volatile("movq 8(%rsi), %r9"); __asm volatile("movq 0(%rsi), %r8"); __asm volatile("movq 72(%rsi), %rsi"); // 必须放在最后恢复
__asm volatile("pushq %rbp");
__asm volatile("" :: : "memory");}
复制代码


这个代码在没有开启编译优化的情况下工作得很好。


但是在开启了-O3 优化的时候就出问题了。


接下来我们看一下两种模式下的汇编语句差别:


不开优化:



开启优化:



可以看到,不开优化的时候,函数开头生成了开辟栈帧的汇编指令:


        push    rbp        mov     rbp, rsp        mov     QWORD PTR [rbp-8], rdi        mov     QWORD PTR [rbp-16], rsi
复制代码


函数尾部生成了恢复 rbp 的指令和返回指令:


        nop        pop     rbp        ret
复制代码


而开启了优化选项,函数开头没有开辟栈帧的指令,结尾也只有返回的指令ret


发生这个现象的原因是编译优化选线会增加-fomit-frame-pointer优化选项,此优化选项会避免生成栈帧。


如下在-O0 优化等级增加此优化选项:



可以看到增加-fomit-frame-pointer选项之后,不再生成开辟栈帧的指令(但是和-O3 优化不完全一样)。


想要解决不同优化级别生成指令不一致的问题,大致有两种方案:


  1. 修改编译参数

  2. 修改代码

解决方案 1:修改编译参数

对于方案 1,又分为两种思路,一种是在不开启优化的时候手动增加-fomit-frame-pointer的优化选项,另一种是在开启优化的时候指定-fno-omit-frame-pointer,禁止栈指针的优化选项。但是第二种思路有问题,经过实际测试,-O3优化选项会覆盖-fno-omit-frame-pointer,导致-fno-omit-frame-pointer选项不生效。因此只能不指定-O3,而是根据gcc的文档,手动开启除了-fomit-frame-pointer的所有优化选项。


暂且不论此方案的实现繁琐程度,此方案最大的问题在于gcc优化选项会对所有的函数生效,而对于其他函数,原本不应该受到影响,所以此方案不合适。

解决方案 2:修改代码

对于此方案,又分为两种思路:


  1. 将 switch_to 函数使用纯汇编文件实现,而不是通过gcc内嵌汇编

  2. 使用gcc相关的属性控制,不生成开栈指令


对于思路 1,这里不做描述,实际测试确实可以解决问题,但是带来的新问题是引入了汇编文件,编译时需要特殊处理一下。这里重点讨论一下思路 2。


在过去的某个时间点,我知道有相关的属性可以禁止 gcc 生成开栈指令的,但是具体是什么指令我给忘了,搜索引擎上也没有查到(应该是搜索姿势不对)。直到后来有一天,在一本书上看到:


为了演示特定的编译结果,这里使用 GCC 的 naked 语法让 test 不产生函数的入口和出口。


这段话出自《深入 Linux 内核架构和底层原理》,作者是刘京洋,书的质量很高,在此安利一波。

主角登场

于是我便在函数定义时添加了__attribute__((naked))属性,查看了一下生成的汇编指令,如下(内嵌汇编修改了一部分):



从上面的汇编结果可以看出,在不开启优化的情况下,也没有生成开栈指令,也没有生成返回指令(返回指令对应的是内嵌汇编中写的)。开启优化的结果是一样的。


根据 gcc 的文档说明:


Use this attribute on the ARM, AVR, C4x and IP2K ports to indicate that the specified function does not need prologue/epilogue sequences generated by the compiler. It is up to the programmer to provide these sequences.


大意就是编译器不生成函数序言/结尾指令,而由程序员提供这些序列。这在手写内嵌汇编是很有用的。

总结

__attribute__((naked))属性是 GCC 扩展出来的,它告诉编译器不为该函数生成任何函数序言或结尾。这在程序员需要手动管理堆栈框架和寄存器的特殊情况下很有用。此属性的常见应用程序包括编写汇编语言功能,实现上下文切换和编写中断处理程序。

发布于: 刚刚阅读数: 4
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018-10-13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
gcc属性__attribute__((naked))使用场景_GCC_SkyFire_InfoQ写作社区