오늘은 프로그램 build 시에 개별 source code들이 어떤 형식으로 compile 되고 생성된 relocatable object 파일들이 어떤게 연결되는지에 대해서 알아보겠습니다.
추가로, 지난 번 다뤘던 EFL Format의 Section 중 Symbol Table에 대해서도 다루도록 하겠습니다.
아래 예제로 두 source code를 compile 하면, 각각 relocatable object 파일을 생성하게 됩니다.
hwjung@jhaewon-z01:~$ cat functionA.c
int functionA(int n) {
return n + 10;
}
hwjung@jhaewon-z01:~$ cat functionMain.c
#include <stdio.h>
int functionA(int n);
int main(int argc, char *argv[]) {
int a = 0;
a = functionA(a);
printf("a is %d\n", a);
return 0;
}
hwjung@jhaewon-z01:~$ file functionA.o
functionA.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
hwjung@jhaewon-z01:~$ file functionMain.o
functionMain.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
하지만, 아직 두 object 파일이 연결되지 않은 상태이기 때문에, main 함수가 있는 object 파일을 disassebly 해보면 functionA의 주소가 아직 설정되어 있지 않은 것을 알 수 있습니다.
이는 당연히 functionA가 functionMain이 아닌 functionA에서 구현되어 있기 때문입니다.
hwjung@jhaewon-z01:~$ objdump -d functionMain.o
functionMain.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
1a: 8b 45 fc mov -0x4(%rbp),%eax
1d: 89 c7 mov %eax,%edi
1f: e8 00 00 00 00 callq 24 <main+0x24> # It should be functionA()
24: 89 45 fc mov %eax,-0x4(%rbp)
27: 8b 45 fc mov -0x4(%rbp),%eax
2a: 89 c6 mov %eax,%esi
2c: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 33 <main+0x33>
33: b8 00 00 00 00 mov $0x0,%eax
38: e8 00 00 00 00 callq 3d <main+0x3d> # It should be printf()
3d: b8 00 00 00 00 mov $0x0,%eax
42: c9 leaveq
43: c3 retq
functionMain의 object 파일에서 Relocation section을 보면, 다음과 같이 functionA와 printf 함수와 관련된 정보가 있습니다.
hwjung@jhaewon-z01:~$ readelf -r functionMain.o
Relocation section '.rela.text' at offset 0x2a0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000c00000004 R_X86_64_PLT32 0000000000000000 functionA - 4
00000000002f 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000039 000d00000004 R_X86_64_PLT32 0000000000000000 printf - 4
Relocation section '.rela.eh_frame' at offset 0x2e8 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
이렇게 functionA 함수의 주소를 알 수 없기 때문에, relocatable object 파일들을 Linking 해줌으로써, functionA()와 printf() 함수의 주소가 올바르게 설정될 수 있습니다.
주소가 올바르게 설정되면 프로그램이 call instruction을 호출할 때 어디로 Jump해야 하는지를 알 수가 있게 됩니다.
두 relocatable object 파일들을 Link로 연결해주면 다음과 같이 dynamically Linked로 표시되는 프로그램이 생성됩니다.
hwjung@jhaewon-z01:~$ gcc -o functionCall functionA.o functionMain.o
hwjung@jhaewon-z01:~$ file functionCall
functionCall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f044c849036b1947c121e8103f319f8feb73b878, for GNU/Linux 3.2.0, not stripped
이렇게 생성된 프로그램을 disassembly 해보면 다음과 같이 functionA()와 printf() 함수의 주소가 올바르게 표현되는 것을 확인할 수 있습니다.
hwjung@jhaewon-z01:~$ objdump -d functionCall
functionCall: file format elf64-x86-64
...
0000000000001149 <functionA>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 89 7d fc mov %edi,-0x4(%rbp)
1154: 8b 45 fc mov -0x4(%rbp),%eax
1157: 83 c0 0a add $0xa,%eax
115a: 5d pop %rbp
115b: c3 retq
000000000000115c <main>:
115c: f3 0f 1e fa endbr64
1160: 55 push %rbp
1161: 48 89 e5 mov %rsp,%rbp
1164: 48 83 ec 20 sub $0x20,%rsp
1168: 89 7d ec mov %edi,-0x14(%rbp)
116b: 48 89 75 e0 mov %rsi,-0x20(%rbp)
116f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
1176: 8b 45 fc mov -0x4(%rbp),%eax
1179: 89 c7 mov %eax,%edi
117b: e8 c9 ff ff ff callq 1149 <functionA> # The address of functionA is 1149
1180: 89 45 fc mov %eax,-0x4(%rbp)
1183: 8b 45 fc mov -0x4(%rbp),%eax
1186: 89 c6 mov %eax,%esi
1188: 48 8d 3d 75 0e 00 00 lea 0xe75(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
118f: b8 00 00 00 00 mov $0x0,%eax
1194: e8 b7 fe ff ff callq 1050 <printf@plt>
1199: b8 00 00 00 00 mov $0x0,%eax
119e: c9 leaveq
119f: c3 retq
결국, functionA.o object 파일에 있는 .symtab section에 functionA가 정의되어 있기 때문에, 이 relocatable object 파일을 연결해야 gcc 입장에서 functionA.o 파일에 functionA() 함수가 있다는 것을 알 수가 있습니다.
그렇다면, 실제 .symtab section에 어떠한 정보가 있는지 알아보겠습니다.
아래 .symtab section을 보면, object 파일에 functionA 함수 정보가 있습니다. 그 중 "Ndx" Column은 Section Header의 Index 번호를 의미하는데, 아래 functionA 함수가 위치한 Section Header가 Index 1번에 위치하기 때문에 해당 Section Header가 무엇인지 확인해야 합니다.
hwjung@jhaewon-z01:~$ readelf -s functionA.o
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS functionA.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 4
9: 0000000000000000 19 FUNC GLOBAL DEFAULT 1 functionA
functionA.o object 파일에서 Section Header를 살펴보면, Index 1번은 .text Section인 것을 알 수 있습니다.
hwjung@jhaewon-z01:~$ readelf -S functionA.o
There are 12 section headers, starting at offset 0x258:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000013 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000053
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000053
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .comment PROGBITS 0000000000000000 00000053
0000000000000025 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 00000078
0000000000000000 0000000000000000 0 0 1
[ 6] .note.gnu.propert NOTE 0000000000000000 00000078
0000000000000020 0000000000000000 A 0 0 8
[ 7] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 000001d8
0000000000000018 0000000000000018 I 9 7 8
[ 9] .symtab SYMTAB 0000000000000000 000000d0
00000000000000f0 0000000000000018 10 9 8
[10] .strtab STRTAB 0000000000000000 000001c0
0000000000000017 0000000000000000 0 0 1
[11] .shstrtab STRTAB 0000000000000000 000001f0
0000000000000067 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
위 명령어 결과 중 .symtab section에 있던 value column의 값은 .text section 내에서 functionA 함수가 위치하는 offset을 의미합니다. .symtab section의 value가 0x0이므로 .text section 내의 functionA 함수 위치는 .text section 시작 위치가 됩니다.
hwjung@jhaewon-z01:~$ objdump -d functionA.o
functionA.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <functionA>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 83 c0 0a add $0xa,%eax
11: 5d pop %rbp
12: c3 retq
object 파일들과 마찬가지로 실행 파일도 Linking 할 때, symbol table을 가지게 되고, 이 .symtab section에는 프로그램 내에서 사용되는 모든 함수의 주소가 포함됩니다.
아래 결과를 보시면 symbol table에 functionA와 main 함수가 위치해 있는 것을 알 수 있습니다.
hwjung@jhaewon-z01:~$ readelf -s functionCall
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
...
Symbol table '.symtab' contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
...
53: 0000000000001149 19 FUNC GLOBAL DEFAULT 16 functionA
...
63: 000000000000115c 68 FUNC GLOBAL DEFAULT 16 main
...
한 가지 예외는, library 함수인 printf()의 경우에는 symbol table에 포함되어 있지 않는 것입니다.
printf()와 같은 library 함수는 os에 의해서 제공되며, 프로그램이 실행될 때 printf() 함수가 동적으로 load되면서 사용되게 됩니다. 따라서, 프로그램을 build 할 때는 함수의 주소를 확인할 수가 없습니다.
대신에 이러한 함수의 주소를 저장하기 위해서 PLT(Procedure Linkage Table)이라고 불리는 Table이 존재합니다.
이 PLT를 이용하여 call instruction이 library 함수를 호출할 때 PLT의 entry의 주소를 설정하게 됩니다.
아래 disassembly code를 보면, printf() 함수의 경우 printf@plt 와 같이 표시되는 것을 볼 수 있습니다.
hwjung@jhaewon-z01:~$ objdump -d functionCall
functionCall: file format elf64-x86-64
...
Disassembly of section .text:
...
000000000000115c <main>:
115c: f3 0f 1e fa endbr64
1160: 55 push %rbp
1161: 48 89 e5 mov %rsp,%rbp
1164: 48 83 ec 20 sub $0x20,%rsp
1168: 89 7d ec mov %edi,-0x14(%rbp)
116b: 48 89 75 e0 mov %rsi,-0x20(%rbp)
116f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
1176: 8b 45 fc mov -0x4(%rbp),%eax
1179: 89 c7 mov %eax,%edi
117b: e8 c9 ff ff ff callq 1149 <functionA>
1180: 89 45 fc mov %eax,-0x4(%rbp)
1183: 8b 45 fc mov -0x4(%rbp),%eax
1186: 89 c6 mov %eax,%esi
1188: 48 8d 3d 75 0e 00 00 lea 0xe75(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
118f: b8 00 00 00 00 mov $0x0,%eax
1194: e8 b7 fe ff ff callq 1050 <printf@plt>
1199: b8 00 00 00 00 mov $0x0,%eax
119e: c9 leaveq
119f: c3 retq
별개로, strip 명령어를 통해서 symbol table은 제거될 수 있습니다.
file 명령어로 확인해보면 "not stripped" 메시지가 있으면 symbol table이 존재하는 것이고 stripped로 표현되어 있으면 symbol table이 제거된 것입니다.
hwjung@jhaewon-z01:~$ file functionCall
functionCall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f044c849036b1947c121e8103f319f8feb73b878, for GNU/Linux 3.2.0, not stripped
hwjung@jhaewon-z01:~$ strip functionCall
hwjung@jhaewon-z01:~$ file functionCall
functionCall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f044c849036b1947c121e8103f319f8feb73b878, for GNU/Linux 3.2.0, stripped
hwjung@jhaewon-z01:~$ readelf -s functionCall
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Debugging을 할 때 이렇게 symbol table이 매우 중요합니다. symbol table은 프로그램을 build 할 때 생성되기 때문에 version이나 build number가 달라질 때마다 symbol table에 있는 정보는 계속 달라지게 됩니다.
따라서 dump 분석 시에 올바른 symbol을 맞춰야 제대로된 함수명등을 확인할 수가 있습니다.
'Debugging > Linux' 카테고리의 다른 글
CPU Registers and Instructions - Instructions (0) | 2022.11.05 |
---|---|
CPU Registers and Instructions - Registers (0) | 2022.11.03 |
64bit Stack Walking (0) | 2022.10.14 |
ELF Format #3 - Section Header (0) | 2022.10.09 |
ELF Format #2 - Program Header (0) | 2022.10.08 |