c语言的编译过程详解

    科技2022-08-11  124

    c语言的编译过程详解

    IDE的使用让很多和我一样的人对C/C++可执行程序的底层生成一知半解,不利于我们深入理解原理。在这里小结一下,望路过的大神指正~

    前言:从一个源文件(.c文件)到可执行程序到底经历了哪几步,我想很多人都知道,但是每一步具体做了什么,估计很多像我一样的人就不能说的清清楚楚,明明白白了。

    C语言编译过程分成四个步骤: 1,由.c文件到.i文件,这个过程叫预处理 2,由.i文件到.s文件,这个过程叫编译 3,由.s文件到.o文件,这个过程叫汇编 4,由.o文件到可执行文件,这个过程叫链接

    编辑一个小程序,hello.c

    #include <stdio.h> #include <stdlib.h> int main() { printf("hello world!\n"); return 0; }

    1、预处理

    预处理过程实际上是处理“#”的过程:#include包含的头文件直接拷贝到hello.c中;#define定义的宏定义进行替换,同时删除代码中没有的注释部分…

    具体做的事儿如下:

    (1)将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换

    (2)处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些

    (3)处理#include,将#include指向的文件插入到该行处

    (4)删除所有注释

    (5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行

    (6)保留#pragma编译器指令,因为编译器需要使用

    gcc -E hello.c -o a.c 可以生成预处理以后的文件,通过查看文件内容和文件大小可以得知a.c将stdio.h和stdlib.h包含了进来。一下是在Ubuntu上编译后的文件:

    drwxrwxr-x 2 lpq lpq 4096 1月 10 10:21 ./ drwxrwxr-x 4 lpq lpq 4096 1月 10 10:19 ../ -rw-rw-r-- 1 lpq lpq 42194 1月 10 10:21 a.c -rw-rw-r-- 1 lpq lpq 95 1月 10 10:21 hello.c

    2、编译

    编译的过程实质上是将高级语言翻译成机器语言的过程,即对a.c做了这些事:

    (1)词法分析

    (2)语法分析

    (3)语义分析

    (4)优化后生成相应的汇编代码

    注:高级语言——>汇编语言——>机器语言(二进制)

    gcc -S hello.c -o a.s 可以生成汇编代码,汇编代码如下:

    .file "hello.c" 2 .section .rodata 3 .LC0: 4 .string "hello world!" 5 .text 6 .globl main 7 .type main, @function 8 main: 9 .LFB0: 10 .cfi_startproc 11 pushl %ebp 12 .cfi_def_cfa_offset 8 13 .cfi_offset 5, -8 14 movl %esp, %ebp 15 .cfi_def_cfa_register 5 16 andl $-16, %esp 17 subl $16, %esp 18 movl $.LC0, (%esp) 19 call puts 20 movl $0, %eax 21 leave 22 .cfi_restore 5 23 .cfi_def_cfa 4, 4 24 ret 25 .cfi_endproc 26 .LFE0: 27 .size main, .-main 28 .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" 29 .section .note.GNU-stack,"",@progbits

    3、汇编

    gcc -c hello.c -o a.o 是将源文件翻译成二进制文件,类Unix系统编译的结果生成.o文件,Windows系统生成的是.obj文件。

    所以,编译过程就是讲.c文件翻译成二进制文件。

    汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。

    由于a.o是机器码,不能以纯文本的形式打开(vim打开是乱码的)。

    4、链接

    就像hello.c中使用到了C标准库的东西“printf”,但是编译过程只是将源文件翻译成二进制文件而已,这个二进制文件还不能直接执行,还需要一个动作:将翻译成的二进制文件与需要用到的库绑定在一块。

    gcc hello.c -o a.out 可以生成可执行程序,即gcc不带任何参数。ldd命令可以查看你的可执行程序所依赖的库。

    可以看到a.o的大小是1.1k,毕竟他只是把源文件翻译成二进制文件。a却有7k,应该是他多了很多“绳子”吧。在运行的时候这些“绳子”就将对应的库函数“牵过来”。很形象的比喻是不是?哈哈。libc.so.6 中就对咱们用的printf进行了定义。

    补充:

    编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。

    词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。

    语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。

    语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。

    源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。

    目标代码生成:代码生成器(Code Generator).

    目标代码优化:目标代码优化器(Target Code Optimizer)。

    链接的主要内容是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地衔接。

    链接的主要过程包括:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。

    链接分为静态链接和动态链接。

    静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。

    而动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。

    Processed: 0.025, SQL: 8