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
二、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 文件有一個文件頭(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 文件中存在的重定位表(未確定的跳轉地址)和符號表(函數名稱和對應地址的地址簿)