Linux内核源码阅读系列(6)-链接3Posted by jcadam - 20/01/09 at 02:01 下午 上一篇举例的时候那个例子并不是很恰当,因为用局部变量的生存周期来解释的话,也是行的通的。那个例子只能说是又添加了一种新的解释而已。要找一个比较妖的还挺难,就直接抄书了: /* foo5.c */ #include <stdio.h> void f(void); int x = 15213; int y = 15212; int main() { f(); printf("x = 0x%x y = 0x%x \n", x, y); return 0; } /* bar5.c */ double x; void f() { x = -0.0; ^^^---链接器在处理这个符号x的时候,选择了,foo5.c文件中定义的 “强符号”int型的x,也就是解释为foo5.c中的x的内存位置写入 在这里定义的double型的值。 } 在IA32/Linux机器上,double型是8个字节,而int型是4字节;因此,这里将用double型的”-0.0″覆盖foo5.c中的x和y的内存位置,于是理所当然的程序出了一个意想不到的意外,而且这类错误是不容易被发现。 静态链接库静态链接库就是把一堆相关的 $ gcc -O2 -c main.c $ gcc -static -o swap_sample main.o libswap.a 程序在跟静态库链接的时候,首先链接器会按照命令行输入的从前往后的方向对可重定位文件进行符号解析,找出在模块内部未定义的符号,并将在其后找到包含这个符号定义的那个模块的代码和数据拷贝进入将生成的可执行目标文件,并对其中的符号进行重定位,如果这些未定义的符号全部解决,则链接成功并输出可执行文件,否则链接器会报错。 重定位重定位就是确定一个对象(包括代码和数据)在存储器中的位置的过程。关于每个需要重定位的符号,链接器有两个方面事情要做,1. 对模块中的符号的定义(definition)进行定位,这个工作主要是合并各个输入模块的代码和数据节,并给每个节和每个符号定义赋以新的存储器地址; 2. 将模块中的引用(reference)指向正确的符号定义位置,这个工作主要依靠“重定位表目”完成,也就是上一篇中提到的实例中的 重定位表目可以用下面的包括下面代码展示的内容: typedef struct { int offset; /* 需要被重定位的“引用”在所在节中的偏移量 */ int symbol:24, /* 这个引用应该指向的符号 */ type:8; /* 重定位类型 */ } 最重要的两类重定位类型就是”R_386_PC32″和”R_386_32″。
重定位符号应用的算法伪代码如下: foreach section s { foreach relocation entry r { refptr = s + r.offset; /* 指向需要被重定位的引用的指针 */ /* relocate a PC-relative reference */ if (r.type = R_386_PC32) { refaddr = ADDR(s) + r.offset; /* 引用的运行时地址 */ *refptr = (unsigned) ((ADDR(r.symbol) + *refptr - refaddr); } /* relocate an absolute reference */ if (r.type == R_386_32) *refptr = (unsigned) (ADDR(r.symbol) + *refptr); } } R_386_PC32在没有跳转的情况下,众所周知程序是按照从上到下的顺序顺序执行的,而这个事实在机器语言级别的直接反应就是PC的值默认情况下都会指向(经过call指令的计算后)当前执行指令的邻近下一条指令的地址。IA32结构中,一条指令的大小是4字节,所以,call指令的默认参数总是”-4″(0xfffffc),以便操作数于PC值相加时,跳转到临近的下一条指令。也就是说上面伪代码中的refptr在PC相关的地址引用中,初始值是”-4″。那么,如果程序发生非顺序执行的跳转,其重点因素就是要给call等类似的指令一个正确的操作数。这个操作数与PC中的值进行计算之后可以跳转到相应的对象(代码)保证程序的正确执行。”R_386_PC32″这种类型的重定位过程就是给call或者类似指令计算一个正确的操作数的过程。上面展示的伪代码中的refptr就是这个操作数。因为在给符号定义(definition)定位的过程中,ADDR(r.symbol)是确定的,所以,refptr就是可以计算的。 R_386_32这种情况就简单些,计算方法只是将可重定位引用所在节的首地址和其偏移量相加,这样就能确定符号在虚存中的位置。 未完待续。 ——写完后偷偷修改的分割线——-
Linux内核源码阅读系列(5)-链接2Posted by jcadam - 19/01/09 at 07:01 下午 可重定位目标文件实例解析上回书说到ELF的文件格式,这里看一个真实的例子:用readelf工具窥看一下上篇提到的 $ gcc -O2 -g -c main.c -o main.o $ file swap.o swap.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped file命令的结果显示, ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian ^^^---小端 Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) ^^^---ObjectFile的类型,上篇提到的3种之一 Machine: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 900 (bytes into file) ^^^---这里指明了“节头表”的位置 Flags: 0x0 Size of this header: 52 (bytes) ^^^---ELF头的大小 Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) ^^^---这里指明了“节头表”的大小 Number of section headers: 23 Section header string table index: 20 上面这个就是ELF的头部信息,头部信息主要提供了ELF文件适用的体系结构以及文件各个部分的定位信息,比如“节头表”的位置/大小等。当然还包括“节头表”中记录的数量。 Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000040 000021 00 AX 0 0 16 [ 2] .rel.text REL 00000000 000854 000008 08 21 1 4 [ 3] .data PROGBITS 00000000 000064 000008 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 00006c 000000 00 WA 0 0 4 ^^^---未初始化的全局变量将存放在此,但仔细看他的"Size"就知道,这个节是空的; 事实上它通常就只是一个占位符。.bss这个名字来自于IBM 704汇编语言中的 Block Storage Start指令的首字母缩写,鉴于它只是占位符号,可以记作 Best Save Space。(来自于《深入理解计算机系统》)恩,随你怎么叫它。 [ 5] .debug_abbrev PROGBITS 00000000 00006c 000060 00 0 0 1 [ 6] .debug_info PROGBITS 00000000 0000cc 00006a 00 0 0 1 [ 7] .rel.debug_info REL 00000000 00085c 000060 08 21 6 4 [ 8] .debug_line PROGBITS 00000000 000136 000037 00 0 0 1 [ 9] .rel.debug_line REL 00000000 0008bc 000008 08 21 8 4 [10] .debug_frame PROGBITS 00000000 000170 000050 00 0 0 4 [11] .rel.debug_frame REL 00000000 0008c4 000010 08 21 10 4 [12] .debug_loc PROGBITS 00000000 0001c0 000043 00 0 0 1 [13] .debug_pubnames PROGBITS 00000000 000203 000023 00 0 0 1 [14] .rel.debug_pubnam REL 00000000 0008d4 000008 08 21 13 4 [15] .debug_aranges PROGBITS 00000000 000226 000020 00 0 0 1 [16] .rel.debug_arange REL 00000000 0008dc 000010 08 21 15 4 [17] .debug_str PROGBITS 00000000 000246 00004c 01 MS 0 0 1 [18] .comment PROGBITS 00000000 000292 00002a 00 0 0 1 [19] .note.GNU-stack PROGBITS 00000000 0002bc 000000 00 0 0 1 ^^^---上面是一堆调试用的二进制内容。忽略之没有什么大碍。 [20] .shstrtab STRTAB 00000000 0002bc 0000c5 00 0 0 1 [21] .symtab SYMTAB 00000000 00071c 000120 10 22 15 4 ^^^---符号表。这位神仙是链接过程处理的重点。 [22] .strtab STRTAB 00000000 00083c 000016 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) There are no section groups in this file. There are no program headers in this file. ^^^----“可重定位目标文件”并不包含program header table,这个表用于将目标文件 映射到虚拟存储器,后文详述。 Relocation section '.rel.text' at offset 0x854 contains 1 entries: Offset Info Type Sym.Value Sym. Name 00000012 00001002 R_386_PC32 00000000 swap ^^^---符号的重定位类型,一共有11种,现在遇到的这个是最重要的两种之一的 “与PC相关的地址引用” Relocation section '.rel.debug_info' at offset 0x85c contains 12 entries: Offset Info Type Sym.Value Sym. Name 00000006 00000501 R_386_32 00000000 .debug_abbrev ^^^---符号的重定位类型,最重要之二,“32位地址引用” 0000000c 00000c01 R_386_32 00000000 .debug_str 00000011 00000c01 R_386_32 00000000 .debug_str 00000015 00000c01 R_386_32 00000000 .debug_str 00000019 00000201 R_386_32 00000000 .text 0000001d 00000201 R_386_32 00000000 .text 00000021 00000701 R_386_32 00000000 .debug_line 00000027 00000c01 R_386_32 00000000 .debug_str 00000031 00000201 R_386_32 00000000 .text 00000035 00000201 R_386_32 00000000 .text 00000039 00000901 R_386_32 00000000 .debug_loc 00000065 00001101 R_386_32 00000000 buf Relocation section '.rel.debug_line' at offset 0x8bc contains 1 entries: Offset Info Type Sym.Value Sym. Name 0000002a 00000201 R_386_32 00000000 .text Relocation section '.rel.debug_frame' at offset 0x8c4 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000018 00000801 R_386_32 00000000 .debug_frame 0000001c 00000201 R_386_32 00000000 .text Relocation section '.rel.debug_pubnames' at offset 0x8d4 contains 1 entries: Offset Info Type Sym.Value Sym. Name 00000006 00000601 R_386_32 00000000 .debug_info Relocation section '.rel.debug_aranges' at offset 0x8dc contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000006 00000601 R_386_32 00000000 .debug_info 00000010 00000201 R_386_32 00000000 .text There are no unwind sections in this file. 这一段描述了需要重定位的符号或者代码总览。其中每一个记录都描述了需要重定位的对象存在于哪一节,以及它相对于节开始位置的偏移量。 Symbol table '.symtab' contains 18 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS main.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 6 7: 00000000 0 SECTION LOCAL DEFAULT 8 8: 00000000 0 SECTION LOCAL DEFAULT 10 9: 00000000 0 SECTION LOCAL DEFAULT 12 10: 00000000 0 SECTION LOCAL DEFAULT 13 11: 00000000 0 SECTION LOCAL DEFAULT 15 12: 00000000 0 SECTION LOCAL DEFAULT 17 13: 00000000 0 SECTION LOCAL DEFAULT 19 14: 00000000 0 SECTION LOCAL DEFAULT 18 ^^^---符号表的前14项无须特别关心,因为他们都是编译器自己加的默认值或者调试信息。 15: 00000000 33 FUNC GLOBAL DEFAULT 1 main 16: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap 17: 00000000 8 OBJECT GLOBAL DEFAULT 3 buf ^^^---在main.o文件中真正存在的符号。其中,第一列描述了符号的地址相对于“节”开始的位置的偏移量。 第二列说明的是这个对象的大小,第三列是对象类型,Ndx指明了对象存在在哪个节中,其中"1"是 .text节,"3"是.data节。如果是"UND",它指的是没有在这个ObjectFile中定义的符号,是需 要重定位的对象。 No version information found in this file. 谢谢各位看官的耐心,这玩意看一遍赶紧忘了吧,如果记住就是把大脑当硬盘用了。之所以细节到这个地步,只为了说明一个问题“万事万物都是有原因的”,只要仔细一点都能弄清楚事情到底怎么了,而这种仔细和耐心正是现在国内很多程序员缺少的。我参加过的与中国工程师打交道的软件项目大都让我感慨万千,中国人做事的态度和日本人做事的态度简直没有办法相提并论,不知道什么原因让很多程序员都浮躁的烧到不行……最近参加了一些中文的邮件列表,发现这个风气尤为盛行,难道真的是民族性格问题?话又说回来,同样的工作,日本工程师拿着不止2倍于中国工程师的工资,好歹做事情的态度也应该对得起这份钱。 符号和符号表上面提到的“符号”,就是“链接”阶段需要解决的重点问题的作业对象。链接需要解决被链接在一起的各个模块之间的符号和代码之间的联系,并重定位这些信息,以便生成的机器代码能够正确的跳转,或者正确的引用到某个变量的值。从链接器的角度看,符号可以分为三类 1.在本模块中定义,被其他模块引用的符号,这可能包括非静态(static)函数和非静态变量; 2.在其他模块中定义,本模块引用的符号,这种符号被称为外部符号(external symbol); 3.在本模块中定义并只在本模块中引用的符号,这种符号成为本地符号(local symbol),可能主要包含static函数和static全局变量。值得一提的是,本地符号并不等于程序的本地变量,因为众所周知,本地程序变量是在运行时栈中管理的,他们的生存周期很短,通过pop和push就能瞬间产生和消灭,无需符号表管理。 链接器的符号解析规则本地符号的定义在链接过程中不会有大问题,每个本地符号都有唯一的定义;包括Java或者C++中的重载函数等,编译器会运用规则位有同名不同参数的函数各自产生一个唯一的“内藏”函数名。 全局符号比较麻烦,首先是要按照强弱分类,1.函数,已经初始化的全局变量是“强符号”;2.未初始化的全局变量是“弱符号”。然后是取舍规则,1.同名两强必出错,链接器报错; 2.同名强弱,肯定是选择强者; 3.同名两弱就随便取一个。注意啦,如果这个时候你还没有意识到明明规则存在的重要性的话,真是后知后觉了。此外,还有一个问题,就是为什么链接大批动态链接库时会有莫名其妙的错误?原因就是这些“潜规则”导致的错误了。比如,下面的例子: 文中有注释,c&p注意。 /* foo2.c */ void bar(void); int x = 12345; ^^^---已经初始化的全局变量,“强”符号 int main() { bar(); printf("x = %d\n", x); return 0; } /* bar2.c */ int x; ^^^---未初始化的全局变量,“弱”符号 void bar() { x = 54321; } 这个程序被链接的并运行的话,其结果让程序员大跌眼睛的,x的输出居然还是12345。而实际现实中的问题比这个不知道要复杂多少倍,往往非常难于发现和排除,所以好的命名习惯真的是在体现一个程序员和一个软件开发团队的素养,而不是为了符合CodeStyle做的面子工程。如果你使用gcc作编译器的话,开启-warn-common选项,将帮助你查找重定义的错误。微软的编译器肯定也有这个选项,但是我有n多年都没给Windows写过程序了,实在是不知道。 未完待续。
Linux内核源码阅读系列(4)-链接1Posted by jcadam - 18/01/09 at 10:01 下午 上周工作实在太忙,写blog的事情是一拖再拖……本来准备好继续写Linux内核构成的,但是因为看书看的比较快,刚好看到一个比较关键的地方——程序的链接和载入,而这个部分有牵扯代码生成,机器语言编程,程序实例化和虚拟存储器很多内容,可以说是从程序员的角度理解Linux内核如何执行用户应用程序行为的核心。况且,这个部分牵扯很多不容忽视的“细节”,我害怕如果日子久了,就再也不记得这些内容,于是算是插入一篇,给自己记录下来。 什么是链接大概教科书上都念过这样的经——源代码变成可执行文件一般要经过 1.编译预处理;2.编译;3.汇编;4.链接 这些过程。从使用TurboC那天起,我就被告知程序是这样产生的,但是直到我大学毕业以后的几年里,才真正知道这些事情都是怎么做的。而且,还是从一本卡耐基梅隆大学的本科生前导课程中学到的,由此可见中国所谓的大学误人子弟的老师的确不在少数。除了继续被愚弄几年以外,上那个大学又有什么意思?先忘掉将中国国内的计算机教育搞到本末倒置的Windows吧,看看Linux中的典型编译过程是怎样完成的。每个操作系统都提供一种编译驱动程序(compile driver),最典型的例子就是gcc。gcc事实上不是一个单独的程序,而是一组程序的组合。Unix世界的逻辑就是将事情分解和简化然后分发给各个可以相互协作的部分完成,在这个设计思想下,Unix世界产成了很多专注做好一件事情的小程序,比如最简单的yes。然而,又为了解决各个小程序之间的协同工作,Unix工具集中又添加进很多tools driver,比如gcc,这种tools driver的设计思想有点像设计模式中提到的facade(这个词似乎是法语,注意发音),他把一些难以把握细节的小工具进行整合从而为用户提供一个简便的接口。gcc在编译程序的过程中实际上需要调用cpp,cc1,as和ld这些工具来帮助它完成工作,于是编译过程就是个RPG游戏,当然这里需要重点看看角色变换和他们的输入与产出。下面游戏开始: 故事背景一个简单的Swap程序: /* main.c */ void swap(); int buf[2] = {1,2}; int main() { swap(); return 0; } /* swap.c */ extern int buf[]; int *bufp0 = &buf[0]; int *bufp1; void swap() { int temp; bufp1 = &buf[1]; temp = *bufp0; *bufp0 = *bufp1; *bufp1 = temp; } 出场人物
这些程序有的是不能被直接调用的,但有的是可以的。为了将问题简化,还是给gcc添加不同的选项作为驱动来观察比他们之间都发生了什么吧。具体什么选项呢,
gcc手册的第一行就告诉我们“他不是一个人”。这三个选项从后往前分别指明的就是 -E 预处理; -S 编译; -c 汇编;如果不加参数就直接将输入的源文件做到链接,默认情况是一条龙服务。下面的表格简单的列出了各个步骤的命令,输入和输出。
此外,
但如果事情做到这个步,算是可以告一个段落,因为最重要的内容之一“可重定位的目标文件”(relocatable object file)已经生成了,也就是这里出现的 ELFELF的名字不错,elf似乎是德国神话传说中的一种精灵,恰恰也说明了ELF文件在系统中的执行就像是变魔法。跑题了。其实他是Executable and Linkable Format的缩写形式,wikipedia上关于elf的解释包括英文在内都不甚详细,但是还是值得一读的。这种二进制文件格式广泛使用于各种计算机平台。最早的ObjectFile的格式是诞生于贝尔实验室的a.out,知道现在仍然有很多应用程序采用a.out形式运行。
伟大的系统在诞生和发展过程中总能产生一些伟大的部件,甚至有些系统本身已经不存在了,但它的思想或者某些精妙的实现却依然在其他系统中以某种形式存在。这个例子数不胜数,比如上面说到的ELF,再比如研发Plan 9操作系统(现在依然存在)的过程中诞生的unicode和procfs。 ObjectFile有三种形式,1.可重定位目标文件(relocatable object file); 2.可执行目标文件(executable object file); 3.共享目标文件(shared object file)。 +----------------------+ | ELF header | <--帮助链接器解析ObjectFile的信息 +----------------------+ | .text | <--已编译程序的机器代码 +----------------------+ | .rodata | <--只读数据,比如pirntf的格式化字串等 +----------------------+ | .data | <--已经初始化的全局变量 +----------------------+ | .bss | <--未初始化的全局变量 +----------------------+ | .symtab | <--符号表,这个表是提供给链接器使用的,每个OjectFile +----------------------+ | .rel.text | <--可重定位的代码 +----------------------+ | .rel.data | <--可重定位的数据 +----------------------+ | .debug | <--调试符号表 +----------------------+ | .line | <--.text节中机器指令于源程序行号之间的映射表 +----------------------+ | .strtab | <--字符串 +----------------------+ | section header table | <--节头表(section header table) +----------------------+ 更加详细的图表可以在"Linkers and Loaders"一书中看到,点这里。 这些分段被称为“节”(section),并且,在 预知后事如何,且听下回分解吧。下午约了师兄去游泳,4点从图书馆出来背着两块砖头一样的书就去了,被水一泡想说的东西全忘了。
|
|
来自: astrotycoon > 《链接加载》