-
[개발도구] - LinkerLinux/development tool 2023. 6. 4. 00:47
글의 참고
- https://en.wikipedia.org/wiki/Linker_(computing)
- https://people.cs.pitt.edu/~xianeizhang/notes/Linking.html
- https://lwn.net/Articles/531148/
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
글의 내용
- Compile process
" 대부분의 compile process 는 대략적으로 4 개의 components 들에 의해서 수행된다.
1. pre-processor
2. compiler
3. assembler
4. linker" 만약, `gcc -O2 -g -o p main.c swap.c` 명령어를 실행하면 위에 언급된 4 개의 components 들이 어떻게 합을 이루며 실행 파일을 만들어낼까?
step 1. pre-processor (cpp)
" pre-processor 에 의해서 main.c 파일을 ASCII interdiate file 인 main.i 파일로 traslate 한다. 그러나, main.i 파일은 여전히 C code 다. `gcc -E` 명령어를 통해 전처리 프로세스만 별도로 수행할 수 있다.
$ gcc --h .... -E Preprocess only; do not compile, assemble or link.
step 2. C compiler (ccl)
" compiler 에 의해서 main.i 파일을 ASCII assembly 언어 파일인 main.s 로 translate 한다. 여기서 CPU 아키텍처에 독립적인 다양한 opmizations 들이 수행된다. `gcc -S` 명령어를 통해 compiler 만 별도로 수행할 수 있다.
$ gcc --h .... -S Compile only; do not assemble or link.
step 3. assembler (as)
" assembler 에 의해서 main.s 파일을 relocatable obj 파일인 main.o 파일로 translate 한다.
$ gcc --h .... -c Compile and assemble, but do not link.
step 4. linker (ld)
" 마지막으로 linker(ld) 에 의해서 main.o 와 swap.o 를 합쳐서 executable obj 파일인 `p` 를 생성한다. 이 때, 단순히 main.o 와 swap.o 파일만 combine 하지는 않는다. main.o 와 swap.o 에서 외부 라이브러리들을 사용할 것이기 때문에, 해당 함수들에 대한 심볼 정보도 함께 mixing 되서 실행 파일 `p` 가 생성된다. 아래 코드는 main.o, swap.o, system libs 들을 엮어서 실행 파일 `p` 를 생성한다.
$ ld -o p ${SYSTEM_LIBS} ./main.o ./swap.o
step 5. assembler (ld)
" 실행 파일 `p` 를 실행하기 위해서는 shell 에 아래의 명령어를 실행하면 된다. 그렇면, 리눅스는 loader 를 실행한다. loader 는 실행 파일 `p` 에 대응하는 process 를 생성하고, 실행 파일 `p` 의 code, data, bss, heap, stack 섹션을 저장하기 위한 page table 을 생성한다. page table 이 생성되면, 실행 파일 `p` 의 정보들을 processor 의 registers 에 설정한 뒤, `p` 의 entry point 로 jump 한다.
$./p
- 링킹
" 서로 다른 2개의 파일을 연결하려면 어떻게 해야 할까? 예를 들어, A라는 파일에서 B파일의 함수를 사용한다고 할 때, 인클루드만 한다고 될까? 절대 그렇지 않다. 인클루드를 하는 것은 링커에게 A 파일이 링커에게 `나 B파일의 함수를 쓸거야` 라는 것을 말하는 것이다. 즉, 우리는 링커를 통해서 다른 파일의 기능을 사용할 수 있는 것이다. 링커없이 다른 파일의 기능을 사용하려면 반드시 해당 심볼의 주소를 알고 있어야 한다. 링킹이란 A 파일이 B 파일의 기능을 쓸 수 있게 2개의 파일을 적절하게 합치는 과정을 말한다. 즉, A와 B를 합쳐 새로운 C라는 파일을 만들어 낸다. 생성된 C 파일안에는 A와 B의 내용이 합쳐저 있는 것을 확인할 수 있다. 이러한 과정을 `링킹`이라고 한다.
- 주요 목적
" 링커에서 중요한 2가지 업무는 다음과 같다.
" 심볼 매칭 : 심볼이란 주소와 같다. 이 주소를 통해 각 변수 및 함수들을 구분하게 된다. 실제 C 코드의 변수 및 함수들은 컴파일 과정을 통해 심볼이라는 하나의 주소로 변환된다. 예를 들어, A라는 파일에서 B라는 파일의 B-1 함수를 호출하려면, B-1 함수의 심볼을 A 파일의 B-1 함수를 호출하는 코드에 매칭시켜야 한다. 이러한 과정을 `심볼 매칭`이라고 한다.
" 재배치 : 컴파일러와 어셈블러는 *.c 혹 *.s 파일의 코드 섹션과 데이터 섹션을 주소 0번지를 기준으로 생성한다. 링커는 링킹과정에서 이러한 섹션 적절하게 재배치해야 한다.- Options
- [-T <script>] 옵션
" 커스텀 링크 스크립트를 링커에게 알린다. 정말 자주 사용하는 옵션이다. 예시는 아래와 같다.
ld -Tlinker.ld
" 이 옵션은 특정 섹션을 직접 지정해서 사용하기도 한다.
ld -e .start -Ttext 0x7C00
- [-e <entry> | --entry=<entry>] 옵션
" 프로그램의 엔트리 포인트를 명시한다. 주로 심볼을 입력하는데, 주소를 입력할 수도 있다. 예시는 아래와 같다.
ld -Tlinker.ld -e 0x7c00
- m 옵션
" 링킹을 통해 만들어지는 실행 파일의 포맷을 지정한다.
- nostdlib 옵션
" 링킹시에 표준 시스템 라이브러리를 링킹하지 못하게 한다. 표준 시스템 라이브러리라 하면 대개 프로그래밍 언어 스펙에 명시되어 있는 라이브러리를 의미한다. 이러한 프로그래밍 언어에 대한 스펙은 컴파일러 및 운영체제에서 구현한다. 그래서 각 컴파일러 및 운영체제는 프로그래밍 언어에서 제공하는 스펙을 확인해서 표준 라이브러리를 유저 애플리케이션을 만드는 개발자들에게 제공해야 한다.
- nostartfiles 옵션
" C 표준 스타트업 파일은 정해져 있다. 그런데, 이걸 사용하지 않게 하는 옵션이다.
- Sections [참고1]
" 위키피디아 자세히 설명이 되어있지만, `.bss` 섹션은 변수가 선언은 되어있지만, 값이 아직 할당되지 않은 영역을 의미한다. 즉, 초기화가 이루어지지 않은 영역을 의미한다. 근데 이 영역을 별도로 두는 이유가 뭘까? 굳이 DATA 영역과 나눈 이유가 뭘까? 메모리를 아끼기 위해서다. 실제 메모리의 할당은 변수에 선언으로 메모리가 할당되는 것이 아니다. 선언한 변수에 메모리가 할당되려면, 값이 들어가야 한다. 즉, 초기화가 되어야 한다. 이 말은 BSS 섹션 영역은 초기화를 하지 않은 변수들이 들어있는 영역이므로, 메모리를 할당받지 않은 영역이란 소리다. 그러면 이 영역의 사이즈는 굉장히 작을 수 밖에 없다. 이 영역의 초기화는 프로그램이 실제 로딩되면서 초기화된다. 즉, 컴파일 시점에 초기화되는 다른 데이터 섹션들과 다르게 `.bss` 섹션은 런타임에 초기화가 이루어진다.
" 아래 코드를 보면, 왼쪽은 global 변수 a 가 최기화 되지 않았고, 오른쪽은 모든 배열의 요소를 1 로 초기화한다. 이제 2 개의 파일을 파일하고 파일 사이즈를 비교해보자.
uninitialized initialized #include<stdio.h>
int a[10000000];
main()
{
long i;
for(i=0;i<10000000;i++)
printf("%d",a[i]);
}#include<stdio.h>
int a[10000000] = {1};
main()
{
long i;
for(i=0;i<10000000;i++)
printf("%d",a[i]);
}$ ls -l bss
-rwxrwxr-x 1 yohda yohda 8328 Dec 22 23:22 bss$ ls -l no-bss
-rwxrwxr-x 1 yohda yohda 40008344 Dec 22 23:22 no-bss$ size bss
text data bss dec hex filename
1555 600 40000032 40002187 262628b bss$ size no-bss
text data bss dec hex filename
1555 40000616 8 40002179 2626283 no-bss" 컴파일 시에, `.bss` 섹션에 시작 위치와 길이만 오브젝트 파일에 저장한 다음에 프로그램이 로드될 때, 그 값을 토대로 BSS 섹션을 0 으로 초기화한다고 한다(main() 함수로 진입하기전 `.bss` 섹션을 0 으로 초기화한다).
" `.bss` 섹션은 ELF 포맷에서 NOBITS 플래그로 지정된다. 이 `NOBITS` 플래그의 의미는 실행 파일에 해당 영역을 포함시키지 않겠다는 의미다. 대신, 런타임에 할당해달라는 의미다. `Progbits` 플래그도 있는데, 이 플래그는 실행 파일에 해당 섹션의 데이터들이 컴파일 시점에 포함된다는 의미다. 아래는 readelf 명령어를 통해 `.data` 섹션과 `.bss` 섹션을 조회해본 내용이다. `.bss` 섹션이 NOBITS 플래그가 설정된것을 확인할 수 있다.
[ 3] .data PROGBITS 00000000 000110 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000110 000000 00 WA 0 0 4
- Stack 과 BSS
" 사실 코드를 제외하면, 나머지 모든 것들은 .data 섹션에 속한다고 보는 것이 맞다. `.rodata`, `.bss` 같은 섹션들 또한 사실 데이터 섹션이다. size 명령어를 통해서 `.object` 파일 구조를 보면 해당 파일에서 사용하는 섹션들을 확인할 수 있다(cpuid.o 파일에는 `.text`, `.data`, `.bss` 섹션들이 포함되어 있다).
$ size cpuid.o text data bss dec hex filename 115 16 0 131 83 cpuid.o
" 그런데, `.bss` 섹션을 제외한 모든 데이터 섹션들은 컴파일 시점에 실행 파일에 포함된다. 그래서, 실행 파일의 크기가 꾀나 커지게 된다. `.bss` 섹션은 실행 파일에는 포함되어 있지 않는 데이터 영역이다. 실행 파일이 메모리에 로딩되어 실행될 때, 비로소 `.bss` 섹션을 할당받게 된다. 즉, 런타임에 할당된다는 소리다. 실행 파일의 크기를 줄이는 것이 뭐가 그렇게 대수일까? 위에 `Sections` 글을 다시 읽고오자. 그리고, 거기서 예제 코드의 파일 사이즈를 다시 확인해보자.
... section .bss stack_bottom: resb 64 stack_top: ... global start section .text bits 32 start: mov esp, stack_top ; print `OK` to screen ...
" 위 코드를 보면, C 코드로 점프하기 전에 stack 을 할당하고 있다. 그런데, stack 영역을 `.bss` 섹션에 할당하는 것처럼 보인다. 왜, stack 영역을 `.bss` 섹션에 할당할까? stack 은 `.bss` 섹션처럼 초기화가 필요없는 데이터 영역이다. 그 이유는 2가지가 있다.
1. PUSH 및 POP 명령어가 순차적으로 증감한다. 예를 들어, PUSH를 하면 ESP가 4바이트(-4) 감소한다. 다시 PUSH를 하면 4바이트 감소(-8)한다. 또 다시 PUSH 하면 4바이트(-12)가 감소한다. 이제 POP을 한다. 그러면 4바이트가 증가(-8)한다. 스택은 이렇게 PUSH와 POP 명령어를 통해서만 접근하면 순차적으로 증감하는 특징이 있다.
2. 정상적인 환경이라면 PUSH 가 POP 보다 선행된다." 데이터를 사용하기 전 초기화를 진행하는 이유는 접근하는 영역에 쓰레기값이 있을 수 있기 때문이다. PUSH와 POP은 그런 걱정을 없애준다. 왜냐면, 값을 사용하기(POP) 전에 반드시 초기화(PUSH)가 되기 때문이다. 결론적으로, 스택은 초기화가 필요 없다. 초기화가 필요없는 것이라면 BSS도 마찬가지다. 그러므로, 스택은 BSS 쪽에 할당받는 것이 나름의 타당성이 있어 보인다. 위에 코드를 보면, BSS 섹션 아래에 `stack_bottom` 심볼과 `stack_top` 심볼을 선언해서 스택의 끝점과 시작점으로 선언하려는 의도이다.
'Linux > development tool' 카테고리의 다른 글
[GIT] - git diff를 통한 patch 파일 생성 및 적용 (0) 2023.08.03 Shell script (0) 2023.07.10 dd (0) 2023.06.10 [개발 도구] objdump (0) 2023.06.10 [개발도구] - inline assembly (0) 2023.06.07