08 ELF 和靜態鏈接:爲什麼程序無法同時在 Linux 和 Windows 下運行?

一、編譯、鏈接、裝載

一段 C 語言程序,通過編譯器編譯成彙編代碼,彙編代碼通過彙編器變成機器碼,於是 CPU 就可以執行這些機器碼

1.1 說的簡單,具體怎麼個流程

舉個栗子

add_lib.c

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

link_example.c

// 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 和 objdump 兩個命令,打印出程序上面兩個文件的彙編代碼和機器碼

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:     文件格式 elf64-x86-64

Disassembly of section .text:

0000000000000000 <add>:
// add_lib.c
int add(int a, int b) {
   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
	return a + b;
   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:     文件格式 elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
// link_example.c
#include <stdio.h>
int main()
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	48 83 ec 10          	sub    rsp,0x10
	int a = 10;
   8:	c7 45 f4 0a 00 00 00 	mov    DWORD PTR [rbp-0xc],0xa
	int b = 5;
   f:	c7 45 f8 05 00 00 00 	mov    DWORD PTR [rbp-0x8],0x5
	int c = add(a, b);
  16:	8b 55 f8             	mov    edx,DWORD PTR [rbp-0x8]
  19:	8b 45 f4             	mov    eax,DWORD PTR [rbp-0xc]
  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 fc             	mov    DWORD PTR [rbp-0x4],eax
	printf("c=%c\n", c);
  2d:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  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

objdump 的兩個文件,程序的地址都是從 0 開始,地址是一樣的,通過 call 指令調用函數,無法知道跳轉到哪一個文件
不管是運行報錯或是彙編代碼裏面的重複地址,不能執行的原因本質上是因爲 add_lib.o 或 link_example.o 並不是可執行文件,而是目標文件
只有通過鏈接器把多個目標文件以及調用的各種函數庫鏈接起來,才能得到可執行文件

1.2 生成可執行文件

使用 -o 參數

gcc -o link_example add_lib.o link_example.o

運行一下 ./link_example
運行可執行文件 link_example

二、C 語言 - 彙編代碼 - 機器碼

第一部分:編譯、彙編、鏈接三個階段組成,最後生成一個可執行文件
第二部分:通過裝載器把可執行文件裝載到內存,CPU 從內存中讀取指令和數據,來開始真正執行程序
編譯、彙編、鏈接生成可執行文件

三、ELF 格式和鏈接:理解鏈接過程

使用 objdump 指令查看可執行文件的內容

objdump -d -M intel -S link_example

命令執行結果

link_example:     文件格式 elf64-x86-64

Disassembly of section .init:

00000000000004f0 <_init>:
 4f0:	48 83 ec 08          	sub    rsp,0x8
 4f4:	48 8b 05 ed 0a 20 00 	mov    rax,QWORD PTR [rip+0x200aed]        # 200fe8 <__gmon_start__>
 4fb:	48 85 c0             	test   rax,rax
 4fe:	74 02                	je     502 <_init+0x12>
 500:	ff d0                	call   rax
 502:	48 83 c4 08          	add    rsp,0x8
 506:	c3                   	ret    

Disassembly of section .plt:

0000000000000510 <.plt>:
 510:	ff 35 aa 0a 20 00    	push   QWORD PTR [rip+0x200aaa]        # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
 516:	ff 25 ac 0a 20 00    	jmp    QWORD PTR [rip+0x200aac]        # 200fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
 51c:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]

0000000000000520 <printf@plt>:
 520:	ff 25 aa 0a 20 00    	jmp    QWORD PTR [rip+0x200aaa]        # 200fd0 <printf@GLIBC_2.2.5>
 526:	68 00 00 00 00       	push   0x0
 52b:	e9 e0 ff ff ff       	jmp    510 <.plt>

Disassembly of section .plt.got:

0000000000000530 <__cxa_finalize@plt>:
 530:	ff 25 c2 0a 20 00    	jmp    QWORD PTR [rip+0x200ac2]        # 200ff8 <__cxa_finalize@GLIBC_2.2.5>
 536:	66 90                	xchg   ax,ax

Disassembly of section .text:

0000000000000540 <_start>:
 540:	31 ed                	xor    ebp,ebp
 542:	49 89 d1             	mov    r9,rdx
 545:	5e                   	pop    rsi
 546:	48 89 e2             	mov    rdx,rsp
 549:	48 83 e4 f0          	and    rsp,0xfffffffffffffff0
 54d:	50                   	push   rax
 54e:	54                   	push   rsp
 54f:	4c 8d 05 ca 01 00 00 	lea    r8,[rip+0x1ca]        # 720 <__libc_csu_fini>
 556:	48 8d 0d 53 01 00 00 	lea    rcx,[rip+0x153]        # 6b0 <__libc_csu_init>
 55d:	48 8d 3d fa 00 00 00 	lea    rdi,[rip+0xfa]        # 65e <main>
 564:	ff 15 76 0a 20 00    	call   QWORD PTR [rip+0x200a76]        # 200fe0 <__libc_start_main@GLIBC_2.2.5>
 56a:	f4                   	hlt    
 56b:	0f 1f 44 00 00       	nop    DWORD PTR [rax+rax*1+0x0]

0000000000000570 <deregister_tm_clones>:
 570:	48 8d 3d 99 0a 20 00 	lea    rdi,[rip+0x200a99]        # 201010 <__TMC_END__>
 577:	55                   	push   rbp
 578:	48 8d 05 91 0a 20 00 	lea    rax,[rip+0x200a91]        # 201010 <__TMC_END__>
 57f:	48 39 f8             	cmp    rax,rdi
 582:	48 89 e5             	mov    rbp,rsp
 585:	74 19                	je     5a0 <deregister_tm_clones+0x30>
 587:	48 8b 05 4a 0a 20 00 	mov    rax,QWORD PTR [rip+0x200a4a]        # 200fd8 <_ITM_deregisterTMCloneTable>
 58e:	48 85 c0             	test   rax,rax
 591:	74 0d                	je     5a0 <deregister_tm_clones+0x30>
 593:	5d                   	pop    rbp
 594:	ff e0                	jmp    rax
 596:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
 59d:	00 00 00 
 5a0:	5d                   	pop    rbp
 5a1:	c3                   	ret    
 5a2:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]
 5a6:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
 5ad:	00 00 00 

00000000000005b0 <register_tm_clones>:
 5b0:	48 8d 3d 59 0a 20 00 	lea    rdi,[rip+0x200a59]        # 201010 <__TMC_END__>
 5b7:	48 8d 35 52 0a 20 00 	lea    rsi,[rip+0x200a52]        # 201010 <__TMC_END__>
 5be:	55                   	push   rbp
 5bf:	48 29 fe             	sub    rsi,rdi
 5c2:	48 89 e5             	mov    rbp,rsp
 5c5:	48 c1 fe 03          	sar    rsi,0x3
 5c9:	48 89 f0             	mov    rax,rsi
 5cc:	48 c1 e8 3f          	shr    rax,0x3f
 5d0:	48 01 c6             	add    rsi,rax
 5d3:	48 d1 fe             	sar    rsi,1
 5d6:	74 18                	je     5f0 <register_tm_clones+0x40>
 5d8:	48 8b 05 11 0a 20 00 	mov    rax,QWORD PTR [rip+0x200a11]        # 200ff0 <_ITM_registerTMCloneTable>
 5df:	48 85 c0             	test   rax,rax
 5e2:	74 0c                	je     5f0 <register_tm_clones+0x40>
 5e4:	5d                   	pop    rbp
 5e5:	ff e0                	jmp    rax
 5e7:	66 0f 1f 84 00 00 00 	nop    WORD PTR [rax+rax*1+0x0]
 5ee:	00 00 
 5f0:	5d                   	pop    rbp
 5f1:	c3                   	ret    
 5f2:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]
 5f6:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
 5fd:	00 00 00 

0000000000000600 <__do_global_dtors_aux>:
 600:	80 3d 09 0a 20 00 00 	cmp    BYTE PTR [rip+0x200a09],0x0        # 201010 <__TMC_END__>
 607:	75 2f                	jne    638 <__do_global_dtors_aux+0x38>
 609:	48 83 3d e7 09 20 00 	cmp    QWORD PTR [rip+0x2009e7],0x0        # 200ff8 <__cxa_finalize@GLIBC_2.2.5>
 610:	00 
 611:	55                   	push   rbp
 612:	48 89 e5             	mov    rbp,rsp
 615:	74 0c                	je     623 <__do_global_dtors_aux+0x23>
 617:	48 8b 3d ea 09 20 00 	mov    rdi,QWORD PTR [rip+0x2009ea]        # 201008 <__dso_handle>
 61e:	e8 0d ff ff ff       	call   530 <__cxa_finalize@plt>
 623:	e8 48 ff ff ff       	call   570 <deregister_tm_clones>
 628:	c6 05 e1 09 20 00 01 	mov    BYTE PTR [rip+0x2009e1],0x1        # 201010 <__TMC_END__>
 62f:	5d                   	pop    rbp
 630:	c3                   	ret    
 631:	0f 1f 80 00 00 00 00 	nop    DWORD PTR [rax+0x0]
 638:	f3 c3                	repz ret 
 63a:	66 0f 1f 44 00 00    	nop    WORD PTR [rax+rax*1+0x0]

0000000000000640 <frame_dummy>:
 640:	55                   	push   rbp
 641:	48 89 e5             	mov    rbp,rsp
 644:	5d                   	pop    rbp
 645:	e9 66 ff ff ff       	jmp    5b0 <register_tm_clones>

000000000000064a <add>:
// add_lib.c
int add(int a, int b) {
 64a:	55                   	push   rbp
 64b:	48 89 e5             	mov    rbp,rsp
 64e:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
 651:	89 75 f8             	mov    DWORD PTR [rbp-0x8],esi
	return a + b;
 654:	8b 55 fc             	mov    edx,DWORD PTR [rbp-0x4]
 657:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
 65a:	01 d0                	add    eax,edx
}
 65c:	5d                   	pop    rbp
 65d:	c3                   	ret    

000000000000065e <main>:
// link_example.c
#include <stdio.h>
int main()
{
 65e:	55                   	push   rbp
 65f:	48 89 e5             	mov    rbp,rsp
 662:	48 83 ec 10          	sub    rsp,0x10
	int a = 10;
 666:	c7 45 f4 0a 00 00 00 	mov    DWORD PTR [rbp-0xc],0xa
	int b = 5;
 66d:	c7 45 f8 05 00 00 00 	mov    DWORD PTR [rbp-0x8],0x5
	int c = add(a, b);
 674:	8b 55 f8             	mov    edx,DWORD PTR [rbp-0x8]
 677:	8b 45 f4             	mov    eax,DWORD PTR [rbp-0xc]
 67a:	89 d6                	mov    esi,edx
 67c:	89 c7                	mov    edi,eax
 67e:	b8 00 00 00 00       	mov    eax,0x0
 683:	e8 c2 ff ff ff       	call   64a <add>
 688:	89 45 fc             	mov    DWORD PTR [rbp-0x4],eax
	printf("c=%d\n", c);
 68b:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
 68e:	89 c6                	mov    esi,eax
 690:	48 8d 3d 9d 00 00 00 	lea    rdi,[rip+0x9d]        # 734 <_IO_stdin_used+0x4>
 697:	b8 00 00 00 00       	mov    eax,0x0
 69c:	e8 7f fe ff ff       	call   520 <printf@plt>
 6a1:	b8 00 00 00 00       	mov    eax,0x0
}
 6a6:	c9                   	leave  
 6a7:	c3                   	ret    
 6a8:	0f 1f 84 00 00 00 00 	nop    DWORD PTR [rax+rax*1+0x0]
 6af:	00 

00000000000006b0 <__libc_csu_init>:
 6b0:	41 57                	push   r15
 6b2:	41 56                	push   r14
 6b4:	49 89 d7             	mov    r15,rdx
 6b7:	41 55                	push   r13
 6b9:	41 54                	push   r12
 6bb:	4c 8d 25 f6 06 20 00 	lea    r12,[rip+0x2006f6]        # 200db8 <__frame_dummy_init_array_entry>
 6c2:	55                   	push   rbp
 6c3:	48 8d 2d f6 06 20 00 	lea    rbp,[rip+0x2006f6]        # 200dc0 <__init_array_end>
 6ca:	53                   	push   rbx
 6cb:	41 89 fd             	mov    r13d,edi
 6ce:	49 89 f6             	mov    r14,rsi
 6d1:	4c 29 e5             	sub    rbp,r12
 6d4:	48 83 ec 08          	sub    rsp,0x8
 6d8:	48 c1 fd 03          	sar    rbp,0x3
 6dc:	e8 0f fe ff ff       	call   4f0 <_init>
 6e1:	48 85 ed             	test   rbp,rbp
 6e4:	74 20                	je     706 <__libc_csu_init+0x56>
 6e6:	31 db                	xor    ebx,ebx
 6e8:	0f 1f 84 00 00 00 00 	nop    DWORD PTR [rax+rax*1+0x0]
 6ef:	00 
 6f0:	4c 89 fa             	mov    rdx,r15
 6f3:	4c 89 f6             	mov    rsi,r14
 6f6:	44 89 ef             	mov    edi,r13d
 6f9:	41 ff 14 dc          	call   QWORD PTR [r12+rbx*8]
 6fd:	48 83 c3 01          	add    rbx,0x1
 701:	48 39 dd             	cmp    rbp,rbx
 704:	75 ea                	jne    6f0 <__libc_csu_init+0x40>
 706:	48 83 c4 08          	add    rsp,0x8
 70a:	5b                   	pop    rbx
 70b:	5d                   	pop    rbp
 70c:	41 5c                	pop    r12
 70e:	41 5d                	pop    r13
 710:	41 5e                	pop    r14
 712:	41 5f                	pop    r15
 714:	c3                   	ret    
 715:	90                   	nop
 716:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
 71d:	00 00 00 

0000000000000720 <__libc_csu_fini>:
 720:	f3 c3                	repz ret 

Disassembly of section .fini:

0000000000000724 <_fini>:
 724:	48 83 ec 08          	sub    rsp,0x8
 728:	48 83 c4 08          	add    rsp,0x8
 72c:	c3                   	ret

你會發現,dump 出來的內容很長,這裏面不僅包含彙編指令,還保留了很多別的數據
在 Linux 下,可執行文件和目標文件所使用的是 ELF(Execuatable and Linkable File Format)格式的文件,即可執行與可鏈接文件格式

151 行:call 64a
main 函數裏調用 add 的跳轉地址,不再是下一條指令的地址,而是 add 函數的入口地址,這是 ELF 格式和鏈接器的功勞

ELF 文件:全局可訪問的變量名稱和函數名稱都存儲在符號表裏,相當於一個地址簿,把名字和地址關聯了起來
ELF 文件格式

  • ELF 文件有一個文件頭(File Header),用於表示文件的基本屬性,比如是否爲可執行文件、對應 CPU、操作系統等
  • ELF 文件格式把各種信息分成一個一個的 Section 保存

大部分的 Section 如下:

  • text Section:代碼段或指令段,用來保存程序的代碼和指令
  • .data Section:數據段,用來保存程序裏面設置好的初始化數據信息
  • .rel.text Section:重定位表(Relocation Table),保留的是當前文件裏面那些跳轉地址目前並不知道的,比如 link_example.o 裏面,在 main 函數中調用 add 和 printf 這兩個函數,在鏈接發生之前,我們是不知道該跳轉到哪裏,這些未確定的跳轉地址信息會存儲在重定位表裏
  • symtab Section:符號表(Symbol Table),保留了當前文件裏面定義的函數名稱和對應地址的地址簿

鏈接器
組裝全局符號表:鏈接器會掃描所有輸入的目標文件,然後把所有符號表裏的信息收集起來,構成一個全局的符號表
修正未確定地址:鏈接器根據重定位表,把所有不確定要跳轉地址的代碼,根據符號表裏面存儲的地址(確定的地址???文件中的位置),進行一次修正
合併成可執行代碼:把所有目標文件的對應段進行一次合併,變成可執行代碼

裝載器
鏈接器將目標文件變成可執行文件,裝載器再次執行程序就很簡單了,不需要考慮地址跳轉的問題,只需要解析 ELF 文件,把對應的指令和數據,加載到內存裏面供 CPU 執行即可

四、總結

  • 同樣程序在 Linux 和 Windows 下不能同時執行,原因就是 Linux 和 Windows 這兩個操作系統下的可執行文件的格式不一樣
  • Linux 下可執行文件格式是 ELF
  • Windows 下可執行文件格式是 PE(Portable Executable Format)
  • Linux 下執行 WIndows 程序,需要在 Linux 下準備 PE 格式的裝載器,開源項目 Wine 兼容 PE 格式的裝載器,可以在 Linux 下運行 Windows 程序
  • Windows 提供 WSL(Windows Subsystem for Linux),可以解析和加載 ELF 格式的文件
  • 平時寫的程序不是所有代碼都在一個文件裏編譯執行,而是拆分成多個文件的函數庫,通過一個靜態鏈接的機制,使得不同文件之間分開存儲,又能通過靜態鏈接共同合作,變成一個可執行程序
  • ELF 格式文件實現了靜態鏈接的機制,能共同合作的原因是 ELF 文件中存在的重定位表(未確定的跳轉地址)和符號表(函數名稱和對應地址的地址簿)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章