编译器(Compiler) 是一个将一种语言的指令集转换成等价的另一种语言
的指令集的程序。
我们通常认为编译器总是将像C++一样的高级语言转换成目标计算机硬件能够执行的文件,但是不总是这样的。很多编译器只是将代码转换成汇编语言,还有一些将一种高级语言转换成另外一种高级语言。
以下是对GCC的编译过程的描述:
预处理(Pre-Processing)
预处理程序主要完成的工作是:头文件(Include file)的插入, 宏 (Macro) 的解开,条件编译(Conditional
compilation)的处理 。生成的还是文本文件,可以使用cat等命令参看其内容。
编译(Compiler)
在这个阶段中,GCC首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,GCC把代码翻译成汇编语言。生成的还是文本文件(其中的内容变成了汇编),可以使用cat等命令参看其内容。
汇编(Assembler)
将汇编语言转换成机器语言。生成的是对象文件(object file/code),可是使用readelf和objdump等命令参看其内容。
在对象文件中出现的只有机器码,但是它通常还包含代码执行时需要的数据,重定位信息,注释,栈,用来进行链接和调试
的程序符号(变量和程序的名字) ;
对象文件可以分为三类:
可重定向的对象文件(Relocatable object file)在编译阶段它可以和其它的可重定的对象文件生成可执行的对象文件。
可执行的对象文件(Executable object file)可以直接加载到内存中运行的程序。
共享的对象文件(Shared object file) 它是一种特殊的可重定向的对象文件 ,它还可以被加载到内存中并进行动态链接。
编译器和汇编器生成可重定向的对象文件 ,链接器将这些文件组合在一起生成可执行文件。
链接(Link)
链接就是将参与链接的对象文件合并成一个可执行文件。
链接的第一步可以在编译时完成,也可以在加载时(通过loader)或者运行时完成(通过应用程序)。
第一步中的关键是符号解析(Symbol resolution),静态链接的是静态库,动态链接的动态库;
静态链接是将需要的外部函数和/或符号直接加到生成的可执行代码中的,动态链接是将需要的外部函数和/或符号
分别记录在PLT(procedure linkage table)和GOT(global offset table,它里面存放的是所有可访问的全局变量)中,
在需要的时候通过动态加载机制将对应的表项填好共调用者使用;
第二步中的关键是重定向(Relocation):
编译器和汇编器生成的对象文件都是从零开始寻址的,重定向就是将所有参与链接的对象文件中的相同的部分放在一起,同时给新建立的每个部分(Section)每个符号一个唯一的虚拟地址;
然后将代码中所有的引用更 新,使得它们指向正确的调整过的地址;
实践
环境是:
Linux版本:Linux localhost.localdomain 2.6.9-78.ELsmp #1 SMP Wed Jul 9 15:39:47
EDT 2008 i686 i686 i386 GNU/Linux
GCC版本:gcc version 3.4.6 20060404 (Red Hat 3.4.6-10)
要编译的程序:hello.c
#include <stdio.h>
int main()
{
printf(“Hello World!/n”);
return 0;
}
gcc -E hello.c -o hello.i (预处理)
gcc -S hello.i (编译)
gcc -c hello.s (汇编)
gcc hello.o -o hello (动态链接,GCC默认使用的方式)
./hello (运行)
Hello World!
gcc -shared -fPIC -o libmyhello.so hello.o (建立动态库,要将之变成位置无关代码)
ar cr libmyhello.a hello.o (建立静态库,它不需要特殊的修改,只要打包就好了)
gcc hello.o -o hello (动态链接)
gcc -static hello.o -o hello.static (静态链接)
gcc -o hello main.c -L. –lmyhello (在程序中使用它们)
其他
PIC的意思是位置无关代码,不管将它加载到内存的什么地方都可以正常执行;
ELF文件有两个视图,一个是从程序运行角度来说的(segment),另一个是从程序链接角度来说的(section),
两个视图的情况可以在ELF文件的header中找到。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x5d954 0x5d954 R E 0x1000
LOAD 0x05d954 0x080a6954 0x080a6954 0x00d40 0x020e8 RW 0x1000
NOTE 0x0000b4 0x080480b4 0x080480b4 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
上面是程序a的视图,其中的虚拟地址是通过基地址(Base Address) 来计算的。
基地址是在执行的过程中从下面三个数值计算出来的:
内存加载地址
最大页面大小
程序的可加载段的最低虚地址
简单的说,每个进程地址空间(process address space)被分为两部分,用户空间和内核空间,
用户空间的范围是:0x00000000-0xc0000000 ,内核空间的范围是0xc0000000 以上。
基地基地址计算出来后,只是加上各个segment对应的偏移量就形成了虚拟地址。
08048000-080a6000 r-xp 00000000 fd:00 88175
/root/workspace/test/src/hello.static
080a6000-080a8000 rw-p 0005d000 fd:00 88175
/root/workspace/test/src/hello.static
080a8000-080a9000 rw-p 080a8000 00:00 0
092dd000-092fe000 rw-p 092dd000 00:00 0
bff8b000-c0000000 rw-p bff8b000 00:00 0
ffffe000-fffff000 r-xp 00000000 00:00 0
上面是a运行时使用的虚拟内存区域(Virtual Memory Area–VMA),值得一提是的,最后一个segment
是程序的栈,它的结束地址是c0000000 ,开始地址是系统计算的。
参考1:http://hi.baidu.com/ahli/blog/item/45ddb31b48ab581c8718bfb4.html
参考2:http://www.linuxforums.org/misc/understanding_elf_using_readelf_and_objdump.html
参考3:http://www.linuxjournal.com/article/6463