ELF와 Linker

유명현·2024년 6월 17일

ELF는 Excuatable and Linking Format을 의미하며, 말 그대로 실행 가낭한 그리고 링크를 하는 형식을 말합니다.

ELF 형식을 따르는 .o (object file)을 분석해보겠습니다.

link가 가능한 object file을 만들면 이런 file을 relocatable file이라고 부릅니다. 나중에 link를 통해서 재배치가 가능하다는 의미입니다.

위에는 Link하기전의 object file (.o)은 Linking View이며, Link가 끝난 후에 완전한 실행 가능한 형태가 된 ELF 형식을 Excution View라고 부릅니다.

Symbol table '.symtab' contains 20 entries

Num: Value Size Type Bind Vis Ndx

0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC LOCAL DEFAULT 5 t 2: 0000000a 0 FUNC LOCAL DEFAULT 5 $d 3: 00000000 0 FILE LOCAL DEFAULT ABS spaghetti.c 4: 00000000 0 OBJECT LOCAL DEFAULT 6 .bss$7 5: 00000000 0 SECTION LOCAL DEFAULT 6 .bss 6: 00000000 0 OBJECT LOCAL DEFAULT 7 .data$10 7: 00000000 0 SECTION LOCAL DEFAULT 7 .data 8: 00000000 0 OBJECT LOCAL DEFAULT 8 .data$15 9: 00000000 0 SECTION LOCAL DEFAULT 8 .data 10: 00000000 0 SECTION LOCAL DEFAULT 5 .text 11: 00000000 0 OBJECT LOCAL DEFAULT 9 Cdebug_frame1
12: 00000000 0 NOTYPE LOCAL DEFAULT ABS BuildAttributesTHUMBIS13:000000000FUNCWEAKDEFAULTUNDLibTHUMB_IS 13: 00000000 0 FUNC WEAK DEFAULT UND LibRequest$$armlib
14: 00000000 4 OBJECT GLOBAL DEFAULT 6 zi
15: 00000000 4 OBJECT GLOBAL DEFAULT 7 rw
16: 00000000 4 OBJECT GLOBAL DEFAULT 8 relocate
17: 00000000 10 FUNC GLOBAL DEFAULT 5 main
18: 00000000 0 FUNC GLOBAL DEFAULT UND __main
19: 00000000 0 FUNC GLOBAL DEFAULT UND _main

Symbol table의 내용 중 Name은 Linker를 위한 symbol 이름이고, Value는 각 section에서의 해당 symbol의 시작 offset 주소를, Size는 Symbol의 크기 (Symbol이 function이나 object가 아닌 경우에는 0), Type은 Function, Object, Section 등을 나타내며, Bind는 Symbol의 scope를 한정 (Local, Global, Weak) 합니다.

약간의 참고로, Ndx에 보시면, UND는 현재 file에서 사용되고 있지만, 실제 함수의 define이 없는 경우를 의미하며, ABS는 relocate 되어서는 안 되는 것을 의미해요 그리고, Ndx= 1이면,.text section을 의미하고요, Ndx=3이면, .data를 의미하는 거죠. Symbol의 scope를 한정하는 것 중 WEAK의 경우는 LOCAL로만 쓰일 가능성이 큰 symbol을 의미하지요.

자세히 보시면, main()은 FUNC (function)이며, GLOBAL로 처리 되고, zi, rw, relocate 역시, GLOBAL variable로 처리가 되어 있음을 확인할 수 있습니다. 다른 file의 함수 등에서 이 global 변수나, 함수를 만지거나, 호출하게 되는 경우 linker가 바로 이 table을 이용하여, 서로를 엮어주는 역할을 하게 됩니다. compile된 file이 수천 개가 될 경우, 이에 대한 table들을 서로 전부다 엮어야 되니까, linker는 해야 하는 일이 엄청 많겠죠?
이런 식으로 따져 가다 보면, - ELF specification을 전부다 분석하다가 진짜로 무엇을 하려고 했는지, 잊어버릴 수도 있으니까, 실제 분석은 ELF specification을 보시는 것이 도움이 되겠습니다. 덜덜덜 - 실제적인 ELF의 형식을 다음 그림과 같이 표현할 수 있겠습니다.

결국 link시에 실제 함수 정의부의 위치와 전역변수들의 위치를 library file과 object file에서 차례대로 조사한 후에 Table로 간직하고 있다가, 그 주소를 함수호출 코드 부분에 기록해 넣는 것이 Linker가 하는 일.

위에 그림을 Linker Placement Rule이라고 해서, section끼리 모으는 역할을 합니다.

Output section안에는 많은 input section들이 있을 텐데, 요놈들을 알파벳 순서대로 다시 정렬해서 하나의 output section을 만듭니다.

Symbol은 절대 Address를 가질 수 있는 최소의 단위라고 했습니다. (함수, 전역변수) 이런 과정을 유식한 말로, Symbol reference resolving이라고 부르는데 다시 말하면, 여러 개의 object file들은 서로 구멍을 갖고 있다고 보면 쉽습니다. 예를 들어, 어떤 김 아무개 c file내에 handle 이라는 int type의 전역변수가 있다고 하고, 다른 장 아무개 c file에서도 이 handle이라는 전역변수의 값을 최 아무개 함수에서 물컹물컹 만지려고 할 때, 각각의 c file에 대하여 compiler가 object file을 만들어 낼 때는 김 아무개 object file에는 handle을 symbol화 해서 link 시에 이 handle이 위치하는 절대 주소를 가질 수 있게 하며, 장 아무개 object file에는 어디 있는지 모르겠지만 handle이라는 symbol이름만 넣어놓고 구멍을 내 놓게 되는 것이죠. 전체를 아우른 대왕 executable object를 만드는 이때! linker는 이런 구멍을 눈치채고, 장 아무개 object file에 compiler가 구멍을 내 놓은 자리에 절대 번지를 써 넣으면, 비로서 장 아무개 object file내에 최 아무개 함수는 handle이라는 전역변수를 물컹물컹 만질 수 있게 되는 원리 입니다. 헥헥.

결국 link시에 실제 함수 정의부의 위치와 전역벼수들의 위치를 library file과 object file에서 차례대로 조사한 후에 모두 table로 간직하고 있다가, 그 주소를 함수호출 코드 부분에 기록해 넣는 것이 linker가 하는 일이라고 할 수 있습니다.

뭉그러져있던 section들이 이제 자세히 보이시는지 모르겠습니다. Linker가 이 object file을 parsing을 하고, 해석을 할 수 있게 하기 위한 내용들이 section들에 들어가 있습니다. 구조는 맨 앞에 ELF header가 있고, 맨 밑에 section header table이 꼭 있습니다. 지붕과 바닥처럼 자리잡고, 그 사이에 section들이 오게 되는데, 각 section에 대하여 간단히 소개하겠습니다. 친절하지 않은 설명입니다.
.rel.text, .text에 들어있는 각 머신 코드의 위치를 나타내고요,
이것들은 나중에 linker가 이 오브젝트 파일을 다른 오브젝트 파일들과 연결시킬 때 필요해요.
.text : 일전에 말했듯이, compile된 기계어 (op code)가 들어 있습니다.
.rodata : read-only data를 의미하며, const로 선언된 바뀌지 않는 data들이 들어 있습니다. 또한 참고로, switch case문에 의한 jump table도 들어 있습니다.
.data : 초기화된 전역변수들이 자리잡고 있습니다.
.bss : 일전에 소개한 대로 초기화 되지 않아, 0으로 초기화 되는 전역변수들이 들어 있습니다. 이런 전역변수들은 실제로 이 section에 자신의 크기만큼 잡히지는 않습니다. - 0으로 초기화 할껀데, 굳이 넣어둘 필요 없겠죠 -
.symtab : symbol table이며 , symbol이란 실제 주소를 가질 수 있는 단위를 말합니다. 보통은 전역변수이름과 함수이름이며, 어떤 사람들은 compile option에 -g option을 꼭 써야, symbol 정보가 생성되는 것으로 알고 있는데, 쓰지 않더라도, 이 section은 꼭 생성됩니다.
.rel.text : relocatable text이며, 말 그대로 op code가 들어 있습니다만, symbol reference resolving에서 언급한 구멍 난 text가 들어 가게 됩니다. executable object에는 없는 section입니다. 결국엔 이것들은 나중에 linker가 이 오브젝트 파일을 다른 오브젝트 파일들과 연결시킬 때 필요해요.
.rel.data : rel.text와 마찬가지로, 구멍 난 전역변수들이 들어 갑니다. 즉, 현재의 파일에서는 정의되어 않고, link시에 참조되는 전역 변수에 대한 재배치 정보를 담고 있고요, extern 전역변수나, extern 함수의 이름들이 들어 있어요.
.debug : 이 section이야 말로 -g option에 의한 debug symbol table입니다. 지역, 전역 변수들에 대한 디버깅 심볼들이 있고요. 컴파일러가 -g 옵션과 함께 수행될 때 생성되죠. 보통 DWARF형식의 디버깅 심볼들이 들어 있어요.
.line : -g option으로 compile했을 때, text section의 opcode와 원본 C의 line을 연결하여, code를 보면서 debugging 가능하게 해줍니다. 만일 이 정보가 잘못된다면, trace32 (Debugger) 등에서 Tracing할 때, symbol을 찾아도, code를 볼 수가 없습니다. 임시방편으로 y.sourcepath + 명령어가 있긴 하지만요.
.strtab : .symtab와 .debug section에 사용되는 const data인 string등을 가지고 있습니다. 그리고, section header의 section 이름들도 들어 있다죠. 헉헉.

linker는 object file을 ram에다가 차곡차곡 쌓아 두면서, 이런 정보들을 가지고, executable elf를 만들어 냅니다. 구멍 난 곳에는 구멍을 메워주고 - 외부 함수로 branch했었어야 되는데 정보가 없어서 그냥 구멍으로 놔주었던 곳에는 진짜 주소를 메워 넣고, 외부 전역변수를 사용했었어야 되는 곳에는 전역변수 주소를 끼워 넣습니다. 맨 먼저 나왔던 그림을 조금 더 아름답게 꾸며 보자면 아래와 같습니다.

만약 여러개의 전역변수가 동일한 이름을 여러 군대에 선언된다면 어떻게 될까요?
Complier에게는 두 가지 종류의 Global symbol이 있습니다. Strong 또는 Weak 인데요. 함수와 초기화가 된 전역변수는 Strong으로 분류하고, 초기화가 되지 않은 전역변수는 Weak로 분류 합니다.

  1. 여러개의 Strong Symbol은 Linker error를 유발합니다.
  2. 하나의 Strong Symbol과 여러 개의 Weak Symbol이 있다면, Strong Symbol을 선택합니다.
  3. 여러 개의 Weak Symbol이 있다면, 아무거나 하나 골라서 선택합니다.

또한, bss에 관련한 이야기를 하자면, data와 bss에 대한 차이점을 짚고 넘어갸아 하는데요. data는 초기화가 되어 있으니까, Flash등의 ROM에 그 초기 값을 가지고 있을 필요가 없습니다.
하지만, bss는 uninitialize되어 있으니까, 모두 0으로 일단 초기화를 하게 됩니다. 그러니까, 굳이 ROM에 그 초기값을 가지고 있을 피요는 없고, 그 시작 주소와 size만 알면, 그 시작주소에서 크기만큼만 RAM에 확보해주면 됩니다.

RO의 시작 번지는 0x8000이고요, RO가 끝나는 지점부터 RW, ZI가 끝나는 지점부터 Stack과 Heap을 잡게 되는 겁니다. ZI의 크기는 Linker가 다 link 해 봐야만 아는 거니까, Heap과 ZI는 Linker가 Link를 다 끝내는 막장에 Heap과 Stack의 위치를 알아서 Automatic으로 정해 주게 됩니다. 그런데, 우리가 만들어낸 image의 ZI에 포함되지 않는 여기서의 Stack과 Heap은 도대체 뭐냐! 지금 말하고 있는 메모리 Model이 Default Memory 모델임을 잊지 마시고, 현재 RO, RW, ZI는 Embedded OS (또는 Kernel)같은 복잡한 것이 porting되지 않은, Application 하나 올린 거라고 생각하시면 됩니다. 그러니까, 그 Application이 사용하는 Stack과 Standard Library가 사용하는 (malloc같은) Heap인 겝니다. Compiler가 알아서 Stack과 Heap영역을 만들어 준다 고나 할까요. 우리가 직접 Stack과 Heap 영역을 만들어주는 case에는 compiler가 이런 stack과 heap영역을 자동으로 만들어 주는 것을 막아야 합니다.

출처 : 임베디드 레시피

profile
기억보다 기록을

0개의 댓글