以c语言为例,编译器为gcc,讲解程序从编译到运行的过程的产物
// hello.h
/*这个是注释*/
#ifndef __HELLO_H
#define __HELLO_H
#define WELCOME "welcome"
extern char name[];
int hello(void);
#endif
// hello.c
#include "hello.h"
#include <stdio.h>
char name[] = "world";
int hello(void) {
printf("hello %s, %s\n ", name, WELCOME);
return 0;
}
// main.c
#include "hello.h"
int main(void) {
hello();
return 0;
}
// 下面是编译运行
// gcc main.c hello.c
// ./a.out
在链接前,每个源文件都是单独进行处理
一、预处理
//只执行预处理
gcc -E hello.c > hello.i
预处理器是一个小软件(gcc包含)。按例子做如下处理:
1、将例子中的注释行删除,替换成一个空行
2、#ifndef,#endif按条件判断来决定是否使用这段代码,这样避免多文件包含同一个头时重复定义,定义宏__HELLO_H为1,将#ifndef与#endif本来替换成空行
3、#define定义宏WELCOME,将代码中的WELCOME替换成welcome,这行替换成空行
4、#include将对应的头文件内容复制到包含文件中。它有两种形式,#include <> 用于C语言自身库的头文件定义,会搜索系统头文件所在的目录(/usr/include或指定的-I); #include " " 用于所有其它头文件,先搜索当前目录,再搜索系统头文件所在目录,编译时指定的-I目录。所以用户自定义的头文件最好不要用<>,除非编译时添加-I,并把头文件放置在-I路径下;自身库虽然可以用""形式,但搜索时间增加,最好使用<>。用空行替换include所在行
5、有些预处理器指令有编译提示作用,在预处理阶段并不处理,如#pragma。
可以看出预编译器会将删除后的指令替换成一个空行
二、编译
// 大写S
gcc -S hello.c
编译过程分成:词法分析、语法分析、语义分析、中间代码生成、汇编代码生成
这部分略过,只要大致知道编译器根据语言开发者定义的语法表,语法分析器分析一边分析语法以一边去词法分析器里取词,生成语法树,在分析语法的过程中如果发现语法错误报syntax error;语义分析阶段会检查函数实参与形参类型是否匹配,变量是否声明等。
来看看生成的最终汇编代码hello.s,".file"这样前面带.是伪指令
1 .file "hello.c"
2 .text //代码段
3 .globl name //全局变量
4 .data //数据段
5 .type name, @object //name为对象类型
6 .size name, 6 //加上\0的长度
7 name: //name的实现
8 .string "world" //name的值
9 .section .rodata //属于只读数据块
10 .LC0: //局部变量标号,用于表示字符串的地址
11 .string "welcome" //LC0值
12 .LC1: //局部变量标号,用于表示字符串的地址
13 .string "hello %s, %s\n " //LC1值
14 .text //代码段
15 .globl hello //全局变量
16 .type hello, @function // hello是一个函数hello()
17 hello: //下面是hello具体实现
18 .LFB0: //FUNC_BEGIN_LABEL开始标记
19 .cfi_startproc
20 endbr64
21 pushq %rbp
22 .cfi_def_cfa_offset 16
23 .cfi_offset 6, -16
24 movq %rsp, %rbp
25 .cfi_def_cfa_register 6
26 leaq .LC0(%rip), %rdx
27 leaq name(%rip), %rsi
28 leaq .LC1(%rip), %rdi
29 movl $0