编译器指令剖析

前言

日常工作中,经常会遇到一些类似于__attribute__((xxx))样式的“代码”,这些“代码”的作用一般都是告诉编译器,在编译阶段需要干点额外事情来达到我们的要求。这里,姑且将这些“代码”称为编译器指令。本文主要记录一些编译器指令的实现原理以及具体用途。

正文

__ attribute__((noreturn))

告诉编译器,被该指令修饰的函数不会返回,以便编译器可以通过删除那些不会执行的代码来进行优化。

比如下面这段代码👇

void __attribute__((noreturn)) playing(void) {
    do {
        // do something
    } while (1);
    // code will never be executed
}
1
2
3
4
5
6

while(1)之后的代码永远不会执行,即该函数不会返回,则可以通过该编译器指令进行优化。

那么问题来了,编译器究竟删除了哪些代码?是如何优化的?

由于没有找到详细介绍的官方资料,所以只好从汇编层面下手。

以下使用的汇编指令是 Xcode 模拟器生成,所以格式上和 iOS 真机不太一样。具体可以参考这里。

  • 先来看下不加这条编译指令时,函数编译后的汇编代码:
// 源码
void playing(void) {
    do {
        // do something
    } while (1);
    // code will never be executed
}
1
2
3
4
5
6
7
; 汇编
attribute.app`playing:
    0x104825f80 <+0>:  pushq  %rbp            ; 将caller帧指针保存在栈里
    0x104825f81 <+1>:  movq   %rsp, %rbp      ; 将帧指针指向栈顶,准备开始一个新的栈帧
    0x104825f84 <+4>:  jmp    0x104825f89     ; <+9> at ViewController.m:35:5
->  0x104825f89 <+9>:  jmp    0x104825f8e     ; <+14> at ViewController.m:35:5
    0x104825f8e <+14>: movb   $0x1, %al
    0x104825f90 <+16>: testb  $0x1, %al
    0x104825f92 <+18>: jne    0x104825f89     ; <+9> at ViewController.m:35:5
    0x104825f98 <+24>: jmp    0x104825f9d     ; <+29> at ViewController.m:37:1
    0x104825f9d <+29>: popq   %rbp            ; 将caller帧指针从栈里取出
    0x104825f9e <+30>: retq                   ; 返回
1
2
3
4
5
6
7
8
9
10
11
12

popqretq 这两条指令配合使用,便可以完成一个子函数的调用返回。详见这里。

  • 再看下加上这条编译指令后,函数编译后的汇编代码:
// 源码
void __attribute__((noreturn)) playing(void) {
    do {
        // do something
    } while (1);
    // code will never be executed
}
1
2
3
4
5
6
7
; 汇编
attribute.app`playing:
    0x106e72f80 <+0>:  pushq  %rbp            ; 将caller帧指针保存在栈里
    0x106e72f81 <+1>:  movq   %rsp, %rbp      ; 将帧指针指向栈顶,准备开始一个新的栈帧
    0x106e72f84 <+4>:  jmp    0x106e72f89     ; <+9> at ViewController.m:35:5
->  0x106e72f89 <+9>:  jmp    0x106e72f8e     ; <+14> at ViewController.m:35:5
    0x106e72f8e <+14>: movb   $0x1, %al
    0x106e72f90 <+16>: testb  $0x1, %al
    0x106e72f92 <+18>: jne    0x106e72f89     ; <+9> at ViewController.m:35:5
    0x106e72f98 <+24>: jmp    0x106e72f9d     ; <+29> at ViewController.m:37:1
    0x106e72f9d <+29>: ud2                    ; 注意这里,是一条中断指令
1
2
3
4
5
6
7
8
9
10
11

通过对比两次汇编代码,可以看到函数被__attribute__((noreturn))修饰后,尾部的 popret 两条指令会被优化掉,取而代之的是一条 ud2 指令。既然函数被标记了__attribute__((noreturn)),就表明程序不会返回;若返回了,便会序执行到 ud2 这里,引发 EXC_BAD_INSTRUCTION

以上,分析的是 C 函数,而对于 Objective-C 中的类方法和实例方法,经过尝试,该编译器指令并不会生效。至于原因,据Clang文档中描述,编译器可能会诊断被 noreturn 修饰的函数,从而能够使函数仍然可以返回给调用方。

参考资料:

https://clang.llvm.org/docs/AttributeReference.html http://www.keil.com/support/man/docs/armcc/armcc_chr1359124965789.htm

最后更新: 2/1/2021, 5:39:58 PM