GCC内嵌汇编之语法详解 - [Linux]
版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://lyowu.yourblog.org/logs/135012.html
内嵌汇编语法如下: __asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分) 共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。例如: __asm__ __volatile__("cli": : :"memory") 1、汇编语句模板 汇编语句模板由汇编语句序列组成,语句之间使用“;”、“/n”或“/n/t”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。 2、输出部分 输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C 语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。例: __asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) ) 描述符字符串表示对该变量的限制条件,这样GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。 3、输入部分输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。例1 : __asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt)); 例二(bitops.h):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
原创 基于x86的Hello World汇编代码分析(AT&T风格汇编) 收藏
基于x86的Hello World汇编代码分析
(AT&T汇编风格)
本文通过对由gcc对简单C语言代码编译生成的汇编码进行逐句分析解读,来学习x86的汇编结构和堆栈机制。文章涉及细节较多,难免出错,望读者不吝赐教!
一、代码
C语言代码:
/* file: hello.c */
1 #include <stdio.h>
2
3 int add(int a, int b){
4 return (a+b);
5 }
6
7 int main(int argc, char **argv){
8 int a, b, c;
9 a = 3;
10 b = 4;
11 c = add(a, b);
12 printf("a+b=%d/n", c);
13 printf("Hello World!/n");
14 return 0;
15 }
16
gcc -S -ohello.s hello.c输出文件:
/* file: hello.s */
1 .file "hello.c"
2 .text
3 .globl add
4 .type add, @function
5 add:
6 pushl %ebp
7 movl %esp, %ebp
8 movl 12(%ebp), %edx
9 movl 8(%ebp), %eax
10 addl %edx, %eax
11 popl %ebp
12 ret
13 .size add, .-add
14 .section .rodata
15 .LC0:
16 .string "a+b=%d/n"
17 .LC1:
18 .string "Hello World!"
19 .text
20 .globl main
21 .type main, @function
22 main:
23 leal 4(%esp), %ecx
24 andl $-16, %esp
25 pushl -4(%ecx)
26 pushl %ebp
27 movl %esp, %ebp
28 pushl %ecx
29 subl $36, %esp
30 movl $3, -8(%ebp)
31 movl $4, -12(%ebp)
32 movl -12(%ebp), %eax
33 movl %eax, 4(%esp)
34 movl -8(%ebp), %eax
35 movl %eax, (%esp)
36 call add
37 movl %eax, -16(%ebp)
38 movl -16(%ebp), %eax
39 movl %eax, 4(%esp)
40 movl $.LC0, (%esp)
41 call printf
42 movl $.LC1, (%esp)
43 call puts
44 movl $0, %eax
45 addl $36, %esp
46 popl %ecx
47 popl %ebp
48 leal -4(%ecx), %esp
49 ret
50 .size main, .-main
51 .ident "GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2"
52 .section .note.GNU-stack,"",@progbits
二、分析
下面对hello.s进行逐句分析。
第1行为gcc留下的文件信息;第2行标识下面一段是代码段,第3、4行表示这是add函数的入口,第5行为入口标号;6~12行为add函数体,稍后分析;13行为add函数的代码段的大小;14行指示下面是数据段;15~18行定义了main中要用到的两个字符串常量;19行同第二行,20、21行定义了main函数入口,22行为main入口标号。23行开始正式进入main函数,直至49行;50行为main函数代码段体积。51、52行为 gcc留下的信息。
下面从main函数开始单步分析每一句话,并跟踪堆栈状态。
初始状态,堆栈状态如图一:
高 +----------------+ <-- esp (栈顶) 高 +----------------+
| | | | | |
| +----------------+ | + 若干 +
| | | | | |
| +----------------+ | +----------------+ <-- esp
| | | | | |
| +----------------+ | +----------------+
V | | V | |
低 + .... + 低 + .... +
图一 图二
23 leal 4(%esp), %ecx
将esp所指地址加4得到的地址存入ecx。
24 andl $-16, %esp
-16的补码为11...10000,这句话使esp指针下移若干位,新地址末四位是0,故按16字节对齐,如图二。对齐是为了加速CPU访存。
25 pushl -4(%ecx)
将ecx所指地址(也就是程序开始时esp所指位置,如图一所示)的内容压栈。这个内容是eip。关于这句的用途,后面有详细解释。
26 pushl %ebp
将ebp压栈,保存ebp的值,以便在退出函数时恢复。
27 movl %esp, %ebp
将ebp移动到esp的位置。
28 pushl %ecx
将ecx的值压栈,保存,在退出函数时,通过这个值来恢复esp的初始值。
现在,堆栈状态如图三:
高 +----------------+ <-- old esp
| | |
| +---- 若干 ----+
| | |
| +----------------+
| | |
| +----------------+
| | eip | 25 pushl -4(%ecx)
| +----------------+ <-- ebp 27 movl %esp, %ebp
| | old ebp | 26 pushl %ebp (we don't know what old ebp is, but we have to backup it)
| +----------------+ <-- esp
| | old esp+4 | 28 pushl %ecx (ecx =old esp + 4)
| +----------------+
| | |
| +----------------+
V | |
低 + .... +
图三
29 subl $36, %esp
esp向下移动36字节,留出空间给局部变量使用,每个存储单元4字节,故共9格。这里预留的空间有些多,在后续的分析中会发现,很多空都没用上。在第四部分的优化后的代码中也可以看到,36被优化成了20,预留的空间正好用满。
30 movl $3, -8(%ebp)
a = 3, 将a的值存入堆栈(加载到内存中)。
31 movl $4, -12(%ebp)
b = 4, 将b的值存入堆栈(加载到内存中)。
32 movl -12(%ebp), %eax
33 movl %eax, 4(%esp)
将b的值调入寄存器,并且入栈,为调用add函数准备参数。
34 movl -8(%ebp), %eax
35 movl %eax, (%esp)
将a的值调入寄存器,并且入栈,为调用add函数准备参数。
36 call add
调用add函数。注意,在这里,call指令隐含执行了一条push %eip的指令,记录当前代码段执行的位置。
下面进入add函数代码。
6 pushl %ebp
将ebp值压栈保存。
7 movl %esp, %ebp
移动ebp至当前esp位置。
8 movl 12(%ebp), %edx
9 movl 8(%ebp), %eax
将两个参数加载到寄存器。
10 addl %edx, %eax
相加,结果存入eax寄存器。
11 popl %ebp
12 ret
出栈,恢复ebp原来的值,函数返回,结果保存在eax中。注意,在ret指令中隐含执行了pop %eip的指令,从pop出来的eip所指的代码处继续执行。
下面回到main函数中。
37 movl %eax, -16(%ebp)
将函数返回值存入堆栈(内存)。
38 movl -16(%ebp), %eax
将变量c的值加载到寄存器。(此句冗余,编译时加优化选项可消除)
39 movl %eax, 4(%esp)
40 movl $.LC0, (%esp)
将变量c的值和.LC0的地址存入堆栈,为调用printf函数准备参数。
41 call printf
调用printf函数,不跟踪分析。
这个过程中堆栈状态如图四:
高 +----------------+ <-- old esp
| | |
| +---- 若干 ----+
| | |
| +----------------+
| | eip |
| +----------------+
| | old ebp |
| +----------------+
| | old esp+4 |
| +----------------+
| | |
| +----------------+
| | 3 | 30 movl $3, -8(%ebp) a = 3
| +----------------+
| | 4 | 31 movl $4, -12(%ebp) b = 4
| +----------------+
| | 7 | 37 movl %eax, -16(%ebp) eax中为add函数的返回值。
| +----------------+
| | |
| +----------------+
| | |
| +----------------+
| | |
| +----------------+
| | |
| +----------------+
| | 4 / 7 | 33 movl %eax, 4(%esp) / 39 movl %eax, 4(%esp)
| +----------------+ <-- esp (29 subl $36, %esp)
| | 3 / .LC0 | 35 movl %eax, (%esp) / 40 movl $.LC0, (%esp)
| +----------------+
| | eip |
| +----------------+ <-- ebp (7 movl %esp, %ebp)
| | ebp | 6 pushl %ebp
| +----------------+
| | |
| +----------------+
V | |
低 + .... +
图四
42 movl $.LC1, (%esp)
将.LC1地址存入堆栈,注意,这里gcc将printf“偷换”成了puts,所以只传一个参数。
43 call puts
调用puts函数。
44 movl $0, %eax
主函数将要返回0,将0存入eax寄存器。
45 addl $36, %esp
将esp回到函数开始时的位置。
46 popl %ecx
47 popl %ebp
48 leal -4(%ecx), %esp
这三句与程序开始正好相对,恢复寄存器状态到进入函数前的状态。开始的这句话:25 pushl -4(%ecx),存入了esp初始时刻指向单元的内容(应该是eip),但整个程序中都没用上。
49 ret
从main函数返回,返回值由eax带回。图五是图三的拷贝,可以从此图看清楚备份了哪些东西。
高 +----------------+ <-- old esp
| | |
| +---- 若干 ----+
| | |
| +----------------+
| | eip |
| +----------------+
| | old ebp |
| +----------------+
| | old esp+4 |
| +----------------+
| | |
| +----------------+
V | |
低 + .... +
图五
三、总结
分析完这简单的代码后,我们进行一些小小的总结。
1、我们体会一些x86是如何使用堆栈的。堆栈是个动态的空间,在运行的过程中,其中保存的内容主要有两种:局部变量和堆栈转移时保存的指针(寄存器的值)。
2、esp是栈顶指针,pop和push操作将会自动调整esp的值,其他操作,除非esp作为算术运算的结果寄存器外,esp不会改变。个人觉得这里堆栈称之为堆栈有一点点不合理,因为对堆栈的操作并不是完全的pop/push操作的集合,更多的时候是直接通过地址来取数。发生函数调用时,4(%esp)是第一个参数,8(%esp)是第二个参数,依此类推,注意,这里加的4,是隐含指令push %eip导致的。push的操作,首先将esp向低地址方向移动4位,然后在这个单元里存入数据;pop的操作,现从esp所指向的单元里取出数据到指定寄存器,然后将esp向高地址方向移动4位。
3、一个代码段(这里一个函数就是一个代码段)运行时使用堆栈空间中连续的空间,ebp总是指向当前运行中的函数的堆栈空间的第一个位置,也就是基地址的意思。一个代码段在存取自己所使用的数据时总是通过ebp来索引,而获取参数总是通过esp索引。所以在进入一个函数时,必须保存ebp的值,然后将 ebp指向自己的数据其实地址,在退出函数时,恢复ebp的值,使调用它的函数在它返回后能继续正常运行。在main函数开始时改变了esp的值,所以改变之前也需要备份esp的值。
4、函数返回值默认存放在eax寄存器中。
5、寻址方法:
* 立即数寻址:$num,num为数值,也就是字面数值
* 寄存器寻址:%reg,reg为任一寄存器,取出%reg中保存的值
* 寄存器间址:disp(base, index, scale),取出 (disp+base+index*scale) 所表示的内存单元中保存的内容。disp, index, scale都可以省略。
6、main函数中为何要按16字节对齐esp?Linux下面GCC默认的堆栈是16字节对齐的,而这样对齐是为了加快CPU访问效率。这里,不对esp进行16字节对齐并不会影响程序的正确执行。具体的解释参见瀚海xhacker的文章:
http://202.38.64.3/cgi/bbscon?bn=ASM&fn=M47918B7C&num=2388
7、25 pushl -4(%ecx)的作用。(以下解释摘自瀚海foxman和xhacker的帖子)
*** foxman ***
一般来说这不是必需的,当进入一个函数之后,堆栈是这样的
|返回地址 |
|old_ebp | <- ebp
| var1 |
| var2 |
| var3 |
也就是说在一个函数内部,是根据(ebp+4)来找到这个函数返回地址的。
不过对于main函数,进入之后需要堆栈16字节对齐(即andl $-16, %esp),这样就在原
来的main返回地址,与old_ebp之间插入了一些padding字节。为了还能ebp找到main的返
回地址,所以这儿再一次将main的返回地址入栈pushl -4(%ecx),在栈里放置在old_ebp
上方,如下:
| main返回地址 |
| 填充 |
| 填充 |
| main返回地址 |
| old_ebp | <- ebp
| ... |
一般gcc就是这么做的。 这么做主要是为了gcc扩展__builtin_return_address.
__builtin_return_address(LEVEL) 返回当前函数或其调用者的返回地址,参数LEVEL
指定在栈上搜索框架的个数,0 表示当前函数的返回地址,1 表示当前函数的调用
者所在函数的返回地址,依此类推。
这就是根据%ebp来找到返回地址的。
为了能使用__builtin_return_address(0),就需要在push %ebp之前将main返回地
址入栈。如果你不用它,那就没什么问题
*** xhacker ***
另外再加上这个gcc的这个参数
-mpreferred-stack-boundary=x
x=2,3,4 etc,表示栈要2^x字节对齐
cc -mpreferred-stack-boundary=2 -S aa.c
可以看出此时没有那句push -4(%ecx)了,说明正是因为main的对齐,而为了仍然支持
__builtin_return_address扩展加上这条push指令了
***************
四、编译器优化后的代码
gcc -O3 -S -ohello_O3.s hello.c输出文件:
/* file: hello_O3.s */
1 .file "hello.c"
2 .text
3 .p2align 4,,15
4 .globl add
5 .type add, @function
6 add:
7 pushl %ebp
8 movl %esp, %ebp
9 movl 12(%ebp), %eax
10 addl 8(%ebp), %eax
11 popl %ebp
12 ret
13 .size add, .-add
14 .section .rodata.str1.1,"aMS",@progbits,1
15 .LC0:
16 .string "a+b=%d/n"
17 .LC1:
18 .string "Hello World!"
19 .text
20 .p2align 4,,15
21 .globl main
22 .type main, @function
23 main:
24 leal 4(%esp), %ecx
25 andl $-16, %esp
26 pushl -4(%ecx)
27 pushl %ebp
28 movl %esp, %ebp
29 pushl %ecx
30 subl $20, %esp
31 movl $7, 8(%esp)
32 movl $.LC0, 4(%esp)
33 movl $1, (%esp)
34 call __printf_chk
35 movl $.LC1, (%esp)
36 call puts
37 addl $20, %esp
38 xorl %eax, %eax
39 popl %ecx
40 popl %ebp
41 leal -4(%ecx), %esp
42 ret
43 .size main, .-main
44 .ident "GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2"
45 .section .note.GNU-stack,"",@progbits
从代码中,我们看到add函数虽然得到了相应的代码,但并没有被调用,而c=a+b则直接在编译时计算出了其值:7!其它地方并没有太多的优化。函数调用时相应的保存寄存器状态/返回时恢复等结构化的操作都没有改变。
五、进一步讨论
main函数的参数argc,argv是如何传递的?看下面的代码:
/* t.c */
1 #include <stdio.h>
2
3 int main(int argc, char **argv){
4 char *c;
5 if(argc == 1)
6 return 1;
7 else{
8 c = argv[1];
9 puts(c);
10 }
11 return 0;
12 }
13
gcc -S -ot.s t.c的输出文件:
/* g.s */
1 .file "hello.c"
2 .text
3 .globl main
4 .type main, @function
5 main:
6 leal 4(%esp), %ecx
7 andl $-16, %esp
8 pushl -4(%ecx)
9 pushl %ebp
10 movl %esp, %ebp
11 pushl %ecx
12 subl $36, %esp
13 movl %ecx, -28(%ebp)
14 movl -28(%ebp), %eax
15 cmpl $1, (%eax)
16 jne .L2
17 movl $1, -24(%ebp)
18 jmp .L3
19 .L2:
20 movl -28(%ebp), %edx
21 movl 4(%edx), %eax
22 addl $4, %eax
23 movl (%eax), %eax
24 movl %eax, -8(%ebp)
25 movl -8(%ebp), %eax
26 movl %eax, (%esp)
27 call puts
28 movl $0, -24(%ebp)
29 .L3:
30 movl -24(%ebp), %eax
31 addl $36, %esp
32 popl %ecx
33 popl %ebp
34 leal -4(%ecx), %esp
35 ret
36 .size main, .-main
37 .ident "GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2"
38 .section .note.GNU-stack,"",@progbits
这里面,第6~12行与之前相同,备份寄存器,移动esp,为代码段预留数据空间。执行完这一段后,这里,%ecx是一个“指针”,指向%esp+4的位置,也就是存放argc的位置。(注意,这里的指针不完全同于C语言中指针的概念,这里的指针是指某寄存器的值是一个内存单元的地址,C语言中,指针是指某变量的值是一个内存单元的地址。)
13 movl %ecx, -28(%ebp)
将ecx这个“指针”复制到堆栈。
14 movl -28(%ebp), %eax
再把这个“指针”加载到寄存器。
15 cmpl $1, (%eax)
注意,因为%eax中存放的是“指针”,所以这里有括号。(%eax)即为初始时刻的4(%esp)。
16 jne .L2
比较,如果argc!=1,跳转到.L2处。
17 movl $1, -24(%ebp)
18 jmp .L3
如果相等,将main函数欲返回的值存到堆栈中,并且跳转到.L3。
下面看.L2的内容:
20 movl -28(%ebp), %edx
注意,这里-28(%ebp)是指向存放argc单元的“指针”。
21 movl 4(%edx), %eax
再将这个指针向上移动4字节,取出其中的值,即为argv的地址,更准确的说是argv[0]的地址。argv在C语言中是char**型指针。也就是说,%eax-->argv[0],(%eax)==argv[0]
22 addl $4, %eax
%eax(argv[0]的地址)是一个内存地址,加4后就变成argv[1]的地址。(%eax)==argv[1]
23 movl (%eax), %eax
再将这个地址的内容加载到%eax,此时%eax=argv[1]。
24 movl %eax, -8(%ebp)
注意,这里%eax外面没有括号,所以复制的是argv[1],也就是一个char*型的参数。
25 movl -8(%ebp), %eax
将参数加载到寄存器,这句话有些冗余,优化后会被去除。
26 movl %eax, (%esp)
为puts准备参数。
27 call puts
28 movl $0, -24(%ebp)
从puts返回后,准备该分支main函数的返回值,0。可以看到,保存这个返回值的地方同17行。这样,无论从哪个分支出来,都可以直接返回-24(%ebp)的内容。
L3则是函数的一些扫尾工作,不需要再分析了。
六、实践
使用工具Insight跟踪运行hello和t两个文件(gcc -g -o hello hello.s,注意加上-g参数,可以保留汇编代码中的符号信息),观察call语句时push入栈的eip,观察数据段、代码段、堆栈段的地址,观察各寄存器值的变化,体会几种寻址方法。
解读Linux操作系统内核中的GCC特性
上一篇 / 下一篇 2009-04-20 11:53:18
查看( 87 ) / 评论( 0 ) / 评分( 0 / 0 )
Linux内核使用GNU Compiler Collection (GCC)套件的几个特殊功能。这些功能包括提供快捷方式和简化以及向编译器提供优化提示等等。了解这些特殊的 GCC 特性,学习如何在 Linux 内核中使用它们。
GCC和Linux是出色的组合。尽管它们是独立的软件,但是 Linux 完全依靠 GCC 在新的体系结构上运行。Linux 还利用 GCC 中的特性(称为扩展)实现更多功能和优化。本文讨论一些重要的扩展,讲解如何在 Linux 内核中使用它们。
GCC当前的稳定版本(版本4.3.2)支持C标准的三个版本:
•International Organization for Standardization (ISO) 最初的 C 语言标准(ISO C89 或 C90)
•带修正 1 的 ISO C90
•当前的 ISO C99(这是 GCC 使用的默认标准,本文也假设采用这种标准)
注意:本文假设使用 ISO C99 标准。如果指定比 ISO C99 版本旧的标准,那么可能无法使用本文描述的一些扩展。可以在命令行上使用 -std 选项指定 GCC 使用的实际标准。可以通过 GCC 手册查看哪个标准版本支持哪些扩展。
可以以几种方式对可用的 C 扩展进行分类。本文把它们分为两大类:
•功能性 扩展提供新功能。
•优化 扩展帮助生成更高效的代码。
功能性扩展
先讨论一些扩展标准C语言的GCC扩展。
类型发现
GCC 允许通过变量的引用识别类型。这种操作支持泛型编程。在 C++、Ada 和 Java™ 语言等许多现代编程语言中都可以找到相似的功能。Linux 使用 typeof 构建 min 和 max 等依赖于类型的操作。清单 1 演示如何使用 typeof 构建一个泛型宏(见 ./linux/include/linux/kernel.h)。
清单 1. 使用 typeof 构建一个泛型宏
#define min(x, y) ({ /
typeof(x) _min1 = (x); /
typeof(y) _min2 = (y); /
(void) (&_min1 == &_min2); /
_min1 < _min2 ? _min1 : _min2; })
范围扩展
GCC 支持范围,在 C 语言的许多方面都可以使用范围。其中之一是 switch/case 块中的 case 语句。在复杂的条件结构中,通常依靠嵌套的 if 语句实现与清单 2(见 ./linux/drivers/scsi/sd.c)相同的结果,但是清单 2 更简洁。使用 switch/case 也可以通过使用跳转表实现进行编译器优化。
清单 2. 在 case 语句中使用范围
static int sd_major(int major_idx)
{
switch (major_idx) {
case 0:
return SCSI_DISK0_MAJOR;
case 1 ... 7:
return SCSI_DISK1_MAJOR + major_idx - 1;
case 8 ... 15:
return SCSI_DISK8_MAJOR + major_idx - 8;
default:
BUG();
return 0; /* shut up gcc */
}
}
还可以使用范围进行初始化,如下所示(见 ./linux/arch/cris/arch-v32/kernel/smp.c)。在这个示例中,spinlock_t 创建一个大小为 LOCK_COUNT 的数组。数组的每个元素初始化为 SPIN_LOCK_UNLOCKED 值。
/* Vector of locks used for various atomic operations */
spinlock_t cris_atomic_locks[] = { [0 ... LOCK_COUNT - 1] = SPIN_LOCK_UNLOCKED};
范围还支持更复杂的初始化。例如,以下代码指定数组中几个子范围的初始值。
int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
零长度的数组
在 C 标准中,必须定义至少一个数组元素。这个需求往往会使代码设计复杂化。但是,GCC 支持零长度数组的概念,这对于结构定义尤其有用。这个概念与 ISO C99 中灵活的数组成员相似,但是使用不同的语法。
下面的示例在结构的末尾声明一个没有成员的数组(见 ./linux/drivers/ieee1394/raw1394-private.h)。这允许结构中的元素引用结构实例后面紧接着的内存。在需要数量可变的数组成员时,这个特性很有用。
struct iso_block_store {
atomic_t refcount;
size_t data_size;
quadlet_t data[0];
};
判断调用地址
在许多情况下,需要判断给定函数的调用者。GCC 提供用于此用途的内置函数 __builtin_return_address。这个函数通常用于调试,但是它在内核中还有许多其他用途。
如下面的代码所示,__builtin_return_address 接收一个称为 level 的参数。这个参数定义希望获取返回地址的调用堆栈级别。例如,如果指定 level 为 0,那么就是请求当前函数的返回地址。如果指定 level 为 1,那么就是请求进行调用的函数的返回地址,依此类推。
void * __builtin_return_address( unsigned int level );
在下面的示例中(见 ./linux/kernel/softirq.c),local_bh_disable 函数在本地处理器上禁用软中断,从而禁止在当前处理器上运行 softirqs、tasklets 和 bottom halves。使用 __builtin_return_address 捕捉返回地址,以便在以后进行跟踪时使用这个地址。
void local_bh_disable(void)
{
__local_bh_disable((unsigned long)__builtin_return_address(0));
}
常量检测
在编译时,可以使用 GCC 提供的一个内置函数判断一个值是否是常量。这种信息非常有价值,因为可以构造出能够通过常量叠算(constant folding)优化的表达式。__builtin_constant_p 函数用来检测常量。
__builtin_constant_p 的原型如下所示。注意,__builtin_constant_p 并不能检测出所有常量,因为 GCC 不容易证明某些值是否是常量。
int __builtin_constant_p( exp )
Linux 相当频繁地使用常量检测。在清单 3 所示的示例中(见 ./linux/include/linux/log2.h),使用常量检测优化 roundup_pow_of_two 宏。如果发现表达式是常量,那么就使用可以优化的常量表达式。如果表达式不是常量,就调用另一个宏函数把值向上取整到 2 的幂。
清单 3. 使用常量检测优化宏函数
#define roundup_pow_of_two(n) /
( /
__builtin_constant_p(n) ? ( /
(n == 1) ? 1 : /
(1UL << (ilog2((n) - 1) + 1)) /
) : /
__roundup_pow_of_two(n) /
)
函数属性
GCC 提供许多函数级属性,可以通过它们向编译器提供更多数据,帮助编译器执行优化。本节描述与功能相关联的一些属性。下一节描述 影响优化的属性。
如清单 4 所示,属性通过其他符号定义指定了别名。可以以此帮助阅读源代码参考,了解属性的使用方法(见 ./linux/include/linux/compiler-gcc3.h)。
清单 4. 函数属性定义
# define __inline__ __inline__ __attribute__((always_inline))
# define __deprecated __attribute__((deprecated))
# define __attribute_used__ __attribute__((__used__))
# define __attribute_const__ __attribute__((__const__))
# define __must_check __attribute__((warn_unused_result))
清单 4 所示的定义是 GCC 中可用的一些函数属性。它们也是在 Linux 内核中最有用的函数属性。下面解释如何使用这些属性:
•always_inline 让 GCC 以内联方式处理指定的函数,无论是否启用了优化。
•deprecated 指出函数已经被废弃,不应该再使用。如果试图使用已经废弃的函数,就会收到警告。还可以对类型和变量应用这个属性,促使开发人员尽可能少使用它们。
•__used__ 告诉编译器无论 GCC 是否发现这个函数的调用实例,都要使用这个函数。这对于从汇编代码中调用 C 函数有帮助。
•__const__ 告诉编译器某个函数是无状态的(也就是说,它使用传递给它的参数生成要返回的结果)。
•warn_unused_result 让编译器检查所有调用者是否都检查函数的结果。这确保调用者适当地检验函数结果,从而能够适当地处理错误。
下面是在 Linux 内核中使用这些属性的示例。deprecated 示例来自与体系结构无关的内核(./linux/kernel/resource.c),const 示例来自 IA64 内核源代码(./linux/arch/ia64/kernel/unwind.c)。
int __deprecated __check_region(struct resource
*parent, unsigned long start, unsigned long n)
static enum unw_register_index __attribute_const__
decode_abreg(unsigned char abreg, int memory)
优化扩展
现在,讨论有助于生成更好的机器码的一些 GCC 特性。
分支预测提示
在 Linux 内核中最常用的优化技术之一是 __builtin_expect。在开发人员使用有条件代码时,常常知道最可能执行哪个分支,而哪个分支很少执行。如果编译器知道这种预测信息,就可以围绕最可能执行的分支生成最优的代码。
如下所示,__builtin_expect 的使用方法基于两个宏 likely 和 unlikely(见 ./linux/include/linux/compiler.h)。
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
通过使用 __builtin_expect,编译器可以做出符合提供的预测信息的指令选择决策。这使执行的代码尽可能接近实际情况。它还可以改进缓存和指令流水线。
例如,如果一个条件标上了 “likely”,那么编译器可以把代码的 True 部分直接放在分支指令后面(这样就不需要执行分支指令)。通过分支指令访问条件结构的 False 部分,这不是最优的方式,但是访问它的可能性不大。按照这种方式,代码对于最可能出现的情况是最优的。
清单 5 给出一个使用 likely 和 unlikely 宏的函数(见 ./linux/net/core/datagram.c)。这个函数预测 sum 变量将是零(数据包的 checksum 是有效的),而且 ip_summed 变量不等于 CHECKSUM_HW。
清单 5. likely 和 unlikely 宏的使用示例
unsigned int __skb_checksum_complete(struct sk_buff *skb)
{
unsigned int sum;
sum = (u16)csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));
if (likely(!sum)) {
if (unlikely(skb->ip_summed == CHECKSUM_HW))
netdev_rx_csum_fault(skb->dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
}
return sum;
}
预抓取
另一种重要的性能改进方法是把必需的数据缓存在接近处理器的地方。缓存可以显著减少访问数据花费的时间。大多数现代处理器都有三类内存:
•一级缓存通常支持单周期访问
•二级缓存支持两周期访问
•系统内存支持更长的访问时间
为了尽可能减少访问延时并由此提高性能,最好把数据放在最近的内存中。手工执行这个任务称为预抓取。GCC 通过内置函数 __builtin_prefetch 支持数据的手工预抓取。在需要数据之前,使用这个函数把数据放到缓存中。如下所示,__builtin_prefetch 函数接收三个参数:
•数据的地址
•rw 参数,使用它指明预抓取数据是为了执行读操作,还是执行写操作
•locality 参数,使用它指定在使用数据之后数据应该留在缓存中,还是应该清除
void __builtin_prefetch( const void *addr, int rw, int locality );
Linux 内核经常使用预抓取。通常是通过宏和包装器函数使用预抓取。清单 6 是一个辅助函数示例,它使用内置函数的包装器(见 ./linux/include/linux/prefetch.h)。这个函数为流操作实现预抓取机制。使用这个函数通常可以减少缓存缺失和停顿,从而提高性能。
清单 6. 范围预抓取的包装器函数
#ifndef ARCH_HAS_PREFETCH
#define prefetch(x) __builtin_prefetch(x)
#endif
static inline void prefetch_range(void *addr, size_t len)
{
#ifdef ARCH_HAS_PREFETCH
char *cp;
char *end = addr + len;
for (cp = addr; cp < end; cp += PREFETCH_STRIDE)
prefetch(cp);
#endif
}
变量属性
除了本文前面讨论的函数属性之外,GCC 还为变量和类型定义提供了属性。最重要的属性之一是 aligned 属性,它用于在内存中实现对象对齐。除了对于性能很重要之外,某些设备或硬件配置也需要对象对齐。aligned 属性有一个参数,它指定所需的对齐类型。
下面的示例用于软件暂停(见 ./linux/arch/i386/mm/init.c)。在需要页面对齐时,定义 PAGE_SIZE 对象。
char __nosavedata swsusp_pg_dir[PAGE_SIZE]
__attribute__ ((aligned (PAGE_SIZE)));
清单 7 中的示例说明关于优化的两点:
•packed 属性打包一个结构的元素,从而尽可能减少它们占用的空间。这意味着,如果定义一个 char 变量,它占用的空间不会超过一字节(8 位)。位字段压缩为一位,而不会占用更多存储空间。
•这段源代码使用一个 __attribute__ 声明进行优化,它用逗号分隔的列表定义多个属性。
清单 7. 结构打包和设置多个属性
static struct swsusp_header {
char reserved[PAGE_SIZE - 20 - sizeof(swp_entry_t)];
swp_entry_t image;
char orig_sig[10];
char sig[10];
} __attribute__((packed, aligned(PAGE_SIZE))) swsusp_header;
结束语
本文只讨论了在 Linux 内核中可以使用的几个 GCC 特性。可以通过 GNU GCC 手册进一步了解针对 C 和 C++ 语言的所有扩展。另外,尽管 Linux 内核经常使用这些扩展,但是也可以在用户自己的应用程序中使用它们。随着 GCC 的发展,肯定会出现新的扩展,它们会进一步改进性能和增加 Linux 内核的功能。