09GNU C语言程序编译
1. C 语言程序概述
GNU gcc 对 ISO 标准 C89 描述的 C 语言进行了一些扩展,其中一些扩展部分已经包括进 IOS C99 标准中。本节给出了内核中经常用到的一些 gcc 扩展语句的说明。
2. C 程序编译和链接
使用 gcc 汇编器编译 C 语言程序时通常会经过四个处理阶段,即预处理、编译阶段、汇编阶段和链接阶段,如下图所示。
在前处理阶段中,gcc 会把 C 程序传递给 C 前处理器(C 预处理器) CPP,对 C 语言程序中指示符和宏进行替换处理,输出纯 C 语言代码;
在编译阶段,gcc 把 C 语言程序编译生成对应的与机器相关的 as 汇编语言代码;
在汇编阶段,as 汇编器会把汇编代码转换成机器指令,并以特定二进制格式输出保存在目标文件中;
最后 GNU ld 链接器把程序的相关目标文件组合链接在一起,生成程序的可执行映像文件。
调用 gcc 的命令行格式为如下:
gcc [选项][-o outfile] infile ...
其中 infile 是输入的 C 语言文件;outfile 是编译产生的输出文件。对于某次编译过程,并非一定要全部执行这四个阶段,使用命令行选项可以令 gcc 编译过程在某个处理阶段后就停止执行。
gcc -o hello hello.c //编译hello.c程序,生成可执行文件hello
gcc -S -o hello.s hello.c //编译hello.c程序,生成对应汇编程序hello.s。
gcc -c -o hello.o hello.c //编译hello.c程序,生成对应目标文件hello.o而不链接
[root@rockman 0709]# vim hello.c
[root@rockman 0709]# cat hello.c
#include <stdio.h>
int main()
{
printf("Hello world!\n");
return 0;
}
[root@rockman 0709]# gcc -o hello hello.c
[root@rockman 0709]# ls
hello hello.c
[root@rockman 0709]# gcc -S -o hello.s hello.c
[root@rockman 0709]# ls
hello hello.c hello.s
[root@rockman 0709]# cat hello.s
.file "hello.c"
.section .rodata
.LC0:
.string "Hello world!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
.section .note.GNU-stack,"",@progbits
[root@rockman 0709]# gcc -c -o hello.o hello.c
[root@rockman 0709]# ls
hello hello.c hello.o hello.s
[root@rockman 0709]# nm hello.o
0000000000000000 T main
U puts
在编译像 Linux 内核这样包含很多源程序文件的大型程序时,通常使用 make 工具软件对整个程序的编译过程进行自动管理。
3. 圆括号中的组合语句
花括号对 “{...}” 用于把变量声明和语句组合成一个复合语句(组合语句)或一个语句块,这样在语义上这些语句就等同于一条语句。组合语句在花括号后面不需要使用分号。
圆括号中的组合语句,即形如 “({...})” 的语句,可以在 GNU C 中用作一个表达式使用。这样就可以在表达式中使用 loop、switch 语句和局部变量,因此这种形式的语句通常称为语句表达式。语句表达式具有如下示例的形式:
({int y = foo(); int z;
if (y>0) z = y;
else z = -y;
3 + z;})
其中组合语句中最后一条语句必须是后面跟随一个分号的表达式。这个表达式("3 + z")的值即用作整个圆括号括住语句的值。如果最后一条语句不是表达式,那么整个表达式就具有 void 类型,因此没有值。另外,这种表达式中语句声明的任何局部变量都会在整块语句结束后失效。这个示例语句可以像如下形式的赋值语句来使用:
res = x = ({...}) + b;
4. 寄存器变量
GNU 对 C 语言的另一个扩充是允许我们把一些变量值放到 CPU 寄存器中,即所谓寄存器变量。这样 CPU 就不用经常花费较长时间访问内存去取值。寄存器变量可以分为两种:全局寄存器变量和局部寄存器变量。全局寄存器变量会在程序的整个运行过程中保留寄存器专门用于几个全局变量。相反,局部寄存器变量不会保留指定的寄存器,而仅在内嵌 asm 汇编语句中最为输入或输出操作数时使用专门的寄存器。
gcc 编译器的数据流分析功能本身有能力确定指定的寄存器何时含有正在使用的值,何时可派其他用场。当 gcc 数据流分析功能认为存储在某个局部寄存器变量值无用时就可能会删除之,并且对局部寄存器变量的引用也可能被删除、移动或简化。因此,若不想让 gcc 作这些优化改动,最好在 asm 语句中加上 volatile 关键词。
5. 内联函数
在程序中,通常把一个函数声明为内联(inline)函数,就可以让 gcc 把函数的代码集成到调用该函数的代码中去。这样处理可以去掉函数调用时进入/退出事件开销,从而肯定能够加快执行速度。因此把一个函数声明为内联函数的主要目的就是能够尽量快速的执行函数体。另外,如果内联函数中有常数值,那么在编译期间 gcc 就可能用它来进行一些简化操作,因此并非所有的内联函数的代码都会被嵌入进去。内联函数方法对程序代码的长度影响并不明显。使用内联函数的程序编译产生的目标代码可能会长一些也可能会短一些,这需要根据具体情况来定。
内联函数嵌入调用者代码中的操作时一种优化操作,因此只有进行优化编译时才会执行代码嵌入处理。若编译过程中没有使用优化选项 "-0" ,那么内联函数的代码就不会被真正地嵌入到调用者代码中,而只是作为普通函数来处理。把一个函数声明为内联函数的方式时在函数声明是使用关键词 “inline”。
函数中的某些语句用法可能会使得内联函数的替代操作无法正常进行,或者不适合进行替换操作。例如使用了可变参数、内存分配函数 malloca(),可变长度数据类型、非局部 goto 语句、以及递归函数。编译时可以使用选项 -Winline 让 gcc 对标志成 inline 但不能被替换的函数给出警告信息以及不能替换的原因。
当在一个函数定义中既使用 inline 关键词、又使用 static 关键词,即像下面文件 fs/inide.c 中的内联函数定义一样,那么如果所有对该内联函数的调用都被替换而集成在调用者代码中,并且程序中没有引用过该内联函数的地址,则该内联函数自身的汇编代码就不会被引用过。 在这种情况下,除非我们在编译过程中使用选项 -fkeep-inline-fucntions,否则 gcc 就不会再为该内联函数自身生成实际汇编代码。
由于某些原因,一些对内联函数的调用并不能被集成到函数中去。特别是在内联函数定义之前的调用语句是不会被替换集成的,并且也都不能是递归定义的函数。如果存在一个不能被替换集成的调用,那么内联函数就会像平常一样被编译成汇编代码。当然,如果程序中有引用内联函数地址的语句,那么内联函数也会像平常一样被编译成汇编代码。因为对内联函数地址的引用是不能被替换的。
请注意,内联函数功能已经被包括在 ISO 标准 C99 中,但是该标准定义的内联函数与 gcc 定义的有较大区别。ISO 标准 C99 的内联函数语义丁一一等同于这里使用组合关键词 inline 和 static 的定义,即 “省略” 了关键词 static 。若在程序中需要使用 C99 标准的语义,那么就需要使用编译选项 -std=gnu99。不过为了兼容起见,在这种情况下还是最好使用 inline 和 static 组合。以后 gcc 将最终默认使用 C99 的定义,在希望仍然使用这里定义的语义时,就需要使用选项 -std=gnu89 来指定。
若一个内联函数的定义没有使用关键词 static 那么 gcc 就会假设其他程序文件中也对这个函数有调用。因为一个全局符号只能被定义一次,所以该函数就不能再在其他源文件中进行定义。因此这里对内联函数的调用就不能被替换集成。因此,一个非静态的内联函数总是会被编译出自己的汇编代码来。在这方面,ISO 标准 C99 对不使用 static 关键词的内联函数定义等同于这里使用 static 关键词的定义。
如果在定义一个函数时同时指定了 inline 和 extern 关键词,那么该函数定义仅用于内联集成,并且在任何情况下都不会单独产生该函数自身的汇编代码,即使明确引用了该函数的地址也不会产生。这样的一个地址会比那成一个外部引用,就好像你仅仅声明了函数而没有定义函数一样。
关键词 inline 和 extern 组合在一起的作用机会类同一个宏定义。使用这种组合方式就是把带有组合关键词的一个函数定义放在 .h 头文件中,并且把不含关键词的另一个相同函数定义放在一个库文件中。此时头文件中的定义会让绝大多数对该函数的调用被替换嵌入。如果还有没有被替换的对该函数的调用,那么就会使用(引用)程序文件中或库中的拷贝。