본문 바로가기
TIL & WIL

[TIL] 크래프톤 정글 5주차 CS:app 7(링커 Linker)

by 적용1 2024. 4. 19.
728x90

7장 linker

- 링킹(linking)은 여러 개의 코드와 데이터를 모아서 연결하여 메모리에 로드될 수 있고 실행될 수 있는 한 개의 파일로 만드는 작업이다.

 

- 링커는 소포트웨어 개발 시에 독립적인 컴파일을 가능하게 한다.

  큰 규모의 응용프로그램을 한 개의 소스 파일로 구성하는 대신 별도로 수정할 수 있고, 컴파일할 수 있는 더 작은 모듈로 나누어 관리할 수 있다. 이 모듈 중 한 개를 변경할 때, 이 파일만을 간단히 재컴파일하고 이 파일을 다시 링크한다.

 

- 이 단원에서는 리눅스와 표준 ELF-64 목적파일 형식(이후 ELF)을 사용하는 x86-64 시스템의 context에서 논의할 것이다.

7.1 컴파일러 드라이버

 

- 대부분의 컴파일 시스템은 언어 전처리기, 컴파일러, 어셈블러, 링커를 필요에 따라 호출하는 컴파일러 드라이버를 제공한다.

ex) 사용자는 GNU 컴파일 시스템을 사용해서 위 그림의 프로그램을 작성하기 위해 다음과 같은 명령을 쉘에 입력하여 GCC 드라이버를 호출할 수 있다.

 

 

- 위 그림은 이 드라이버가 ASCII 소스 파일에서 예제 프로그램을 실행 목적파일로 번역할 때 드라이버의 동작 내용을 요약한 것이다.(GCC에서 보고 싶으면 -v 옵션으로 실행시켜야 함)

1) 드라이버는 먼저 C 소스 파일 main.c를 ASCII 중간 파일인 main.i로 번역하는 C 전처리기(cpp)를 돌린다.

2) 다음으로 드라이버는 C 컴파일러(ccl)를 돌려서 main.i를 ASCII 어셈블리 언어 파일인 main.s로 번역해준다.

3) 그 다음, 드라이버는 어셈블러(as)를 돌려서 main.s를 재배치 가능 바이너리 목적파일(relocatable object files)인 main.o로 번역한다.

4) 드라이버는 sum.o를 생성하기 위해 동일한 과정을 수행한다. 마지막으로, 링커 프로그램 ld를 실행하는데 링커 프로그램은 실행 가능 목적파일 prog를 생성하기 위해 필요한 시스템 목적파일들과 main.o와 sum.o를 연결한다.

5) 실행 파일 prog를 실행시키려면 Linux 쉘에서 위와 같이 입력하면 된다.

 

-쉘은 로더(loader)라 부르는 운영체제 내의 함수를 호출하며, 로더는 실행파일 prog의 코드와 데이터를 메모리로 복사하고, 제어를 프로그램의 시작 부분으로 전환한다.(PC를 옮기는 거로 보면 될듯함)

7.4 재배치 가능 목적파일

 

- 위 그림은 전형적인 ELF 재배치 가능 목적파일의 형식을 보여준다. ELF 헤더는 목적 파일을 생성한 시스템의 워드 크기와 바이트 순서를 나타내는 16 바이트 배열로 시작한다.

 

- ELF 헤더의 나머지에는 ELF 헤더의 크기, 목적파일 타입(ex : 재배치 가능, 실행 가능, 공유), 머신 타입(ex : x86-64), 섹션 헤더 테이블의 파일 오프셋, 섹션 헤더 테이블의 크기와 엔트리 수가 들어 있다.

 

- 여러 섹션들의 위치와 크기는 섹션 헤더 테이블로 나타내며, 이 테이블은 목적파일의 각 섹션에 대해 고정된 크기의 엔트리를 갖는다.

 

- ELF 헤더와 섹션 헤더 테이블 사이에는 다음과 같은 섹션 내용들이 들어있다.

 

.text : 컴파일된 프로그램의 기계어 코드

 

.rodata : printf 문장의 format string, switch 문의 점프 테이블과 같은 read-only 데이터

 

.data : 초기화된 C 전역변수 및 정적변수. C의 지역 변수들은 런타임에 스택에 저장되며, data나 .bss 섹션에는 나타나지 않는다.

 

.bss : 초기화되지 않은 C 전역변수와 정적변수, 0으로 초기화된 전역변수 및 정적변수. 이 섹션은 목적파일에 실제 공간을 차지하지는 않는다.(위치를 표시하기만 함)

목적파일 형식은 공간 효율성을 위해 초기화된 변수와 초기화되지 않는 변수를 구분한다 : 초기화하지 않은 변수들은 실제 디스크 공간을 차지할 필요가 없기 때문.

런타임에 이 변수들은 메모리에 0으로 초기화되어 할당된다.

 

.symtab : 프로그램에서 정의되고 참조되는 전역변수들과 함수에 대한 정보를 가지고 있는 심볼 테이블.(이 부분을 갖고 있기 때문에 심볼 테이블 정보를 얻기 위해 굳이 -g 옵션을 사용해서 컴파일하지 않아도 됨)

그러나, 컴파일러 내부의 심볼 테이블과 달리, .symtab 심볼 테이블은 지역변수에 대한 엔트리를 가지고 있지 않다.

 

.rel.text : 링커가 이 목적파일을 다른 파일들과 연결할 때 수정되어야 하는 .text 섹션 내 위치들의 리스트. 일반적으로 외부 함수를 호출하거나 전역변수를 참조하는 인스트럭션들은 모두 수정되어야 한다.(지역 함수를 호출하는 것은 안해도 됨)

재배치 정보는 실행 가능 목적파일에는 필요하지 않고, 사용자가 링커에게 명시적으로 이 것을 포함하라고 지시하기 전에는 대개 이 정보는 빠진다.

 

.rel.data : 이 모듈에 의해 정의되거나 참조되는 전역변수들에 대한 재배치 정보. 일반적으로 초기화된 값이 전역변수 또는 외부에 정의된 함수의 주소인 전역변수들은 모두 수정되어야 한다.

 

.debug : 프로그램 내에서 정의된 지역변수들과 typedef, 프로그램과 최초 C 소스 파일에서 정의되고 참조되는 전역변수들을 위한 엔트리를 갖는 디버깅 심볼 테이블.(컴파일러 드라이버가 -g 옵션으로 호출된 경우에 생성)

 

.line : 최초 C 프로그램과 .text 섹션들 내 기계어 인스트럭션 내 라인 번호들 간의 매핑.(컴파일러 드라이버가 -g 옵션으로 호출된 경우에 생성)

 

.strtab : .symtab과 .debug 섹션들 내에 있는 심볼 테이블과 섹션 헤더들에 있는 섹션 이름들을 위한 스트링 테이블. 스트링 테이블은 널 문자로 종료된 스트리의 배열이다.

7.9 실행 가능 목적파일의 로딩

- 실행 가능 목적파일 prog를 실행하기 위해서,  리눅스 쉘의 명령 라인에 다음과 같이 입력할 수 있다.

 

- prog가 내장 쉘 명령어에 대응되지 않기 때문에 쉘은 prog가 실행 가능한 목적파일이라고 가정하며, 쉘은 로더(loader)라고 알려진 메모리 상주 운영체제 코드를 호출해서 이 프로그램을 실행한다.

모든 리눅스 프로그램은 execve 함수를 호출해서 로더를 호출할 수 있다.

 

- 로더는 디스크로부터 실행 가능한 목적파일 내의 코드와 데이터를 메모리로 복사하고 이 프로그램의 첫 번째 인스트럭션(엔트리 포인트)으로 점프해서 프로그램을 실행한다.

 

- 이와 같은 프로그램을 메모리로 복사하고 실행하는 과정을 로딩이라고 부른다.

 

 

- 모든 실행 중인 리눅스 프로그램은 위 그림과 유사한 런타임 메모리 이미지를 가진다.

 

- x86-64 리눅스 시스템에서 코드 세그먼트는 주소 0x400000에서 시작하고, 뒤이어 데이터 세그먼트가 온다.

 

- 런타임 힙(heap)은 데이터 세그먼트 다음으로 오고, malloc 라이브러리를 호출해서 위로 성장한다.

 

- 그 다음은 공유 모듈들을 위해 따로 놔둔 영역이 존재한다.

 

- 사용자 스택은 가장 큰 legal user address에서 시작해서 더 작은 메모리 주소 방향인 아래로 성장한다.

 

- 스택 위의 영역은 운영체제의 메모리 상주 부분인 커널의 코드와 데이터를 위해 따로 놔두었다.

 

- 단순화하기 위해 힙, 데이터, 코드 세그먼트를 붙여서 표시했으며, 스택의 탑을 최대 legal user address에 배치했다.

실제로는 .data 세그먼트의 정렬요건으로 인해 코드와 데이터 세그먼트 사이에 공간이 존재한다.

 

- 링커는 런타임 주소를 스택, 공유 라이브러리, 힙 세그먼트에 할당할 때, 주소 공간 배치 랜덤화(ASLR)를 사용한다.

이들의 위치가 프로그램이 실행될 때 매번 변경되더라도 상대적인 위치는 항상 동일하다.

 

- 실제 로더가 돌아갈 때, 그림 7.15와 유사한 메모리 이미지를 생성한다.

1) 실행 파일 내부의 프로그램 헤더 테이블에 따라 실행 파일의 덩어리를 코드와 데이터 세그먼트로 복사한다.

 

2) 로더는 프로그램의 엔트리 포인트로 점프하며, 이 포인트는 항상 _start 함수의 주소가 된다.

    이 함수는 시스템 목적파일 crtl.o에 정의되어 있으며, 모든 C프로그램에서 이 점은 동일하다.

 

3) _start 함수는 시스템 초기화 함수는 __libc_start_main을 호출하며, 이것은 libc.so에 정의되어 있다.

    이 함수는 실행 환경을 초기화하고, 사용자 수준의 main함수를 호출하고, 리턴 값을 처리하며, 필요한 경우 제어권을

    커널로 넘겨준다.

 

728x90