8. ELF和静态链接:为什么程序无法同时在Linux和Windows下运行?

之前我自己就有一个疑问,为什么同一个程序,在同一台计算机上,在Windows和Linux上不能同时运行,要么只能在Linux上,要么只能在Windows上运行。可是我们并没有换掉CPU,应该可以识别同样的指令呀?

编译、链接和装载:拆解程序的执行

我们之前学到过,写好的C语言程序,可以通过「编译器」编译成汇编代码,然后通过「汇编器」变为CPU可以理解的「机器码」,于是CPU就可以执行这些「机器码」了。但是这个过程是比较笼统的,接下来我们来看看,一个程序是如何变成一个可执行程序的。

我们将之前的add函数示例,拆成两个文件「add_lib.c」和「link_example.c」。

// add_lib.c
int add(int a, int b)
{
    return a+b;
}
// link_example.c

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 5;
    int c = add(a, b);
    printf("c = %d\n", c);
}
$ gcc -g -c add_lib.c link_example.c
$ objdump -d -M intel -S add_lib.o
$ objdump -d -M intel -S link_example.o

add_lib.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
   7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
   a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
   d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  10:   01 d0                   add    eax,edx
  12:   5d                      pop    rbp
  13:   c3                      ret    

link_example.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
   f:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
  16:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
  19:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  1c:   89 d6                   mov    esi,edx
  1e:   89 c7                   mov    edi,eax
  20:   b8 00 00 00 00          mov    eax,0x0
  25:   e8 00 00 00 00          call   2a <main+0x2a>
  2a:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  2d:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
  30:   89 c6                   mov    esi,eax
  32:   48 8d 3d 00 00 00 00    lea    rdi,[rip+0x0]        # 39 <main+0x39>
  39:   b8 00 00 00 00          mov    eax,0x0
  3e:   e8 00 00 00 00          call   43 <main+0x43>
  43:   b8 00 00 00 00          mov    eax,0x0
  48:   c9                      leave  
  49:   c3                      ret    

我们尝试运行「./link_example.o」,但是报错了。我们发现两个程序的地址都是从0开始。如果地址是一样的,程序如果需要通过call指令调用函数,它怎么知道应该跳转到哪一个文件里呢?

无论是这里的运行报错,还是 objdump 出来的汇编代码里面的重复地址,都是因为 「add_lib.o」 以及 「link_example.o」 并不是一个可执行文件(Executable Program),而是目标文件。只有通过「链接器」(Linker)把多个「目标文件」以及「调用的各种函数库」链接起来,我们才可以得到一个「可执行的文件」。

我们通过 gcc 的 -o 参数,可以生成对应的可执行文件,对应执行之后,就可以得到这个简单的加法调用函数的结果。

$ gcc -o link-example add_lib.o link_example.o
$ ./link_example
c = 15

实际上,C语言程序代码 - 汇编代码 - 机器码这个过程,在计算机内部是由两部分组成的。

第一部分,由「编译」,「汇编」以及「链接」三个阶段组成。在这三个阶段完成后,就生成一个「可执行的文件」。

第二部分,通过「装载器」把「可执行文件」装载到「内存」中。CPU从内存中读取指令和数据,来真正执行程序。

ELF 格式和链接:理解链接过程

程序最终通过「装饰器」变成指令和数据的,所以其实我们生成的「可执行代码」只不过是一条条指令。


link_example:     file format elf64-x86-64
Disassembly of section .init:
...
Disassembly of section .plt:
...
Disassembly of section .plt.got:
...
Disassembly of section .text:
...
 6b0:   55                      push   rbp
 6b1:   48 89 e5                mov    rbp,rsp
 6b4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
 6b7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
 6ba:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 6bd:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 6c0:   01 d0                   add    eax,edx
 6c2:   5d                      pop    rbp
 6c3:   c3                      ret    
00000000000006c4 <main>:
 6c4:   55                      push   rbp
 6c5:   48 89 e5                mov    rbp,rsp
 6c8:   48 83 ec 10             sub    rsp,0x10
 6cc:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
 6d3:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
 6da:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
 6dd:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
 6e0:   89 d6                   mov    esi,edx
 6e2:   89 c7                   mov    edi,eax
 6e4:   b8 00 00 00 00          mov    eax,0x0
 6e9:   e8 c2 ff ff ff          call   6b0 <add>
 6ee:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
 6f1:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
 6f4:   89 c6                   mov    esi,eax
 6f6:   48 8d 3d 97 00 00 00    lea    rdi,[rip+0x97]        # 794 <_IO_stdin_used+0x4>
 6fd:   b8 00 00 00 00          mov    eax,0x0
 702:   e8 59 fe ff ff          call   560 <printf@plt>
 707:   b8 00 00 00 00          mov    eax,0x0
 70c:   c9                      leave  
 70d:   c3                      ret    
 70e:   66 90                   xchg   ax,ax
...
Disassembly of section .fini:
...

你会发现,可执行代码 dump 出来内容,和之前的目标代码长得差不多,但是长了很多。因为在Linux下,「可执行文件」和「目标文件」所使用的的都是一种叫「ELF」(Execuatable and Linkable File Format)的文件格式,中文名「可执行与可链接文件格式」,这里面不仅存放了编译成的「汇编指令」,还有其他数据。

比如之前我们所有objdump出来的代码,可以看到对应的函数名称,比如add和main,乃至自己定义的全局变量,都存放在这个ELF格式文件里。这些名字和它们对应的地址,都存储在一个符号表中。「符号表」相当于一个“地址簿”。

在main函数中的调用add的跳转地址,不再是下一条指令的地址了,而是add函数的入口地址了,这就是「EFL格式」和「链接器」的功劳。

ELF格式把各种信息,分成一个个的Section存储起来。ELF有一个基本的文件头,来表示这个文件的基本属性,比如「是否可执行文件」,「对应的CPU」,「操作系统」等等。还有一些其他的Section:

(1).text Section,也叫做「代码段」或者「指令段」,用来保存程序的代码或者指令。

(2).data Section,也叫做「数据段」。用来保存程序里面设置好的e初始化数据信息。

(3).rel.text Secion,「重定位表」,重定位表里,保留的是当前的文件里面,哪些跳转地址其实是我们不知道的。比如上面的 link_example.o 里面,我们在 main 函数里面调用了 add 和 printf 这两个函数,但是在链接发生之前,我们并不知道该跳转到哪里,这些信息就会存储在重定位表里。

(4).symtab Section,「符号表」。符号表保留了我们所说的当前文件里面定义的「函数名称」和「对应地址的地址簿」。

「链接器」会扫描所有输入的「目标文件」,然后把所有「符号表」里面的信息收集起来,构成一个全局的「符号表」。然后再根据「重定位表」,把所有不确定要跳转地址的代码,根据「符号表」里面存储的地址,进行修正。最后将所有目标文件的对应段进行一次合并,变成最终的「可执行代码」。

在链接器把程序变成可执行文件后,要「装饰器」去执行程序就容易多了。「装饰器」无需考虑地址跳转问题,只需要解析ELF文件,把对应的指令和数据,加载到内存里面供CPU执行就好。

总结

为什么同一个程序,在Linux下可以执行而不能再Windows下执行,因为两个操作系统的可执行文件的格式不同

Linux下是「ELF文件格式」,Windows下是「PE文件格式」。Linux下的装饰器只能解析「ELF文件格式」。

如果你想一起学习这门课,可以扫下面的二维码购买:


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容