源代码到可执行代码的过程以及Makefile的编写
注:本文参考《深入理解计算机系统》
1过程的三个步骤
- 预处理。C预处理扩展代码,插入所有用#include命令制定的文件,并扩展所有的用#define声明的宏。
- 编译。编译器产生源代码的汇编代码,汇编器将汇编代码转换成二进制目标代码。目标代码是机器代码的一种形式,包含所有执行的二进制表示,但没有填入地址的全局值。
- 链接。连接器将目标代码与实现库函数的代码合并最终产生可执行的代码文件。可执行代码是机器代码的第二种形式,也就是处理器执行的代码格式。
2.代码示例
这里是一个C语言代码文件matrix.c,目的是实现一个矩阵的转置,包含的过程如下:
#include "matrix.h"
void matrix(int (*pa)[3], int (*pb)[2])
{
int i, j;
for(i = 0; i < 2; i++){
for(j = 0; j < 3; j++){
*(*(pb + j) + i) = *(*(pa + i) + j);
}
}
}
在命令行使用 “-S” 参数,就可以得到 C语言编译器产生的汇编代码
gcc -S matrix.c
这条命令会使GCC运行编译器,产生一个汇编文件matrix.s,但是不做其他进一步工作
汇编代码包含各种声明,部分代码如下 :
matrix:
pushl %ebp
movl %esp, %ebp
pushl %ebx
subl $16, %esp
movl $0, -12(%ebp)
jmp .L2
可以看到,这就是matrix函数的部分汇编代码,每个缩进去的行都对应一条机器执行。
如果我们使用 “ -c ” 命令行选项,GCC会编译并汇编该代码
gcc -c matrix.c
这条命令会产生一个目标代码文件matrix.o,它是二进制格式,要想查看目标代码的内容,可以使用反汇编器,这些程序根据目标代码产生一种类型汇编代码的格式。
objdump -d matrix.o
结果如下:
00000000 <matrix>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 53 push %ebx
4: 83 ec 10 sub $0x10,%esp
7: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
e: eb 49 jmp 59 <matrix+0x59>
10: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)
..........省略..........
5d: 7e b1 jle 10 <matrix+0x10>
5f: 83 c4 10 add $0x10,%esp
62: 5b pop %ebx
63: 5d pop %ebp
64: c3 ret
可以看出上述代码分为四个部分,
- 第一部分是上一行的”00000000 :” 正如之前所说,目标代码是机器代码的一种形式,包含所有执行的二进制表示,但没有填入地址的全局值。
- 第二部分是最左侧的一列,这些代表从“00000000”地址(目前没有填入地址)的偏移量
- 第三部分是中间的16进制字节值,表示了指令代码的字节序列
- 第四部分是最右部分的与GCC生成汇编代码有一些差别的汇编代码
上述第二部分的一些16进制字节值就是汇编指令对应的目标代码,从中可以得到一个重要的信息,机器执行的程序只是对一系列执行进行编码的字节序列。机器对产生这些指令的源代码几乎一无所知。
生成实际可执行的代码需要对一组目标代码文件运行链接器,而这组目标代码文件中必须含有一个main函数,main.c如下:
#include <stdio.h>
#include "matrix.h"
int main()
{
int pa[2][3] = {11, 22, 33, 44, 55, 66};
int pb[3][2] = {0};
int i, j;
matrix(pa, pb);
for(i = 0; i < 3; i++){
for(j = 0; j < 2;j++){
printf("%d\t", *(*(pb + i) + j));
}
printf("\n");
}
return 0;
}
用如下方法生成可执行文件matrix:
gcc -o matrix matrix.o main.c
matrix文件是一个可执行文件,包含两个过程的代码,还包含用来启动和终止程序的信息,以及用来与操作系统交互的信息。
反汇编matrix文件,matrix函数部分的汇编代码如下:
080483f4 <matrix>:
80483f4: 55 push %ebp
80483f5: 89 e5 mov %esp,%ebp
80483f7: 53 push %ebx
80483f8: 83 ec 10 sub $0x10,%esp
80483fb: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
8048402: eb 49 jmp 804844d <matrix+0x59>
8048404: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)
........省略..........
8048451: 7e b1 jle 8048404 <matrix+0x10>
8048453: 83 c4 10 add $0x10,%esp
8048456: 5b pop %ebx
8048457: 5d pop %ebp
8048458: c3 ret
8048459: 90 nop
804845a: 90 nop
804845b: 90 nop
这段代码与matrx.c反汇编生成的代码几乎一样。
链接这个过程就会为所有执行生成一个最左侧的地址空间,就如我们看到的,还有一个是会为全局变量分配一个地址,在编译阶段会是00 00 00 00,在链接这个阶段则会是一个确切的空间地址。
Makefile的编写
编写matrix.h的头文件
#ifndef _MATRIX_H_
#define _MATRIX_H_
//在条件编译之间写所有函数声明和要包含的库函数,例如:
#include <stdio.h>
void matrix(int (*pa)[3], int (*pb)[2]);
#endif
编写matrix.c文件
#include "matrix.h"
//编写所有函数定义
void matrix(int (*pa)[3], int (*pb)[2])
{
int i, j;
for(i = 0; i < 2; i++){
for(j = 0; j < 3; j++){
*(*(pb + j) + i) = *(*(pa + i) + j);
}
}
}
编写main.c文件
#include "matrix.h"
int main()
{
//编写主函数
}
Makefile文件如下:
CC=gcc
FLAG=-c
OUTPUT=-o
ALL=matrix.o main.o
OBJ=matrix
//上面这些都是一些变量,类似C语言的宏定义,但是使用时用“$”引用
$(OBJ):$(ALL) //“:”之前是目标文件,之后是依赖文件 matrix:matrix.o main.o,当在当前目录找不到.o文件则先执行下面的指令
$(CC) $^ $(OUTPUT) $@ //这条缩格的行代表“规则” gcc $^ -o $@
%.o:%c //%代表所有,%.o是所有.o文件,意思是所有.o文件依赖于所有.c文件
$(CC) $(FLAG) $< $(OUTPUT) $@ //规则:gcc -c $< -o $@
.PHONY:clean //.PNONY:假如当前目录有一个名为clean的文件,.PNONY告诉make ":" 后的clean是一个伪目标,make不会执行名为clean的文件而是执行下面的指令
clean: //当make clean时,执行下面的命令 有@符表示执行命令时不显示该命令本身
@rm -fr *.o matrix
@echo "del finshed"
//$@代表当前规则的目标文件
//$<代表当前规则的第一个依赖文件
//$^代表当前规则的所有依赖文件
执行效果如下: