본문 바로가기

Debugging/Linux

Link and Symbol Table

오늘은 프로그램 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