크래프톤정글/CS:APP

CH07 링커 Linking

아람2 2024. 10. 14. 16:01
반응형

CH07 링커 Linking 

링킹 Linking

여러 개의 코드와 데이터를 모아서 연결하여 메모리에 로드될 수 있고 실행될 수 있는 한 개의 파일로 만드는 작업 

링킹은 컴파일 시에 수행할 수 있으며 대게 링커에 의해 처리되고, 링커는 독립적인 컴파일을 가능하게 한다 

 

링킹을 이해하면 큰 프로그램을 작성하는 데 도움이 될 것이다 

링커를 이해하면 위험한 프로그래밍 에러를 피할 수 있게 된다 

링킹을 이해하면 어떻게 언어의 변수 영역 규칙이 구현되었는지 이해하는 데 도움이 된다 

링킹에 대해서 이해하면 다른 중요한 시스템 개념을 이해할 수 있게 된다 

링킹을 이해하면 공유 라이브러리에 대해 이해할 수 있다 

 

7.1 컴파일러 드라이버 

대부분의 컴파일 시스템은 사용자를 대신에서 언어 전처리기, 컴파일러, 어셈블리, 링커를

필요에 따라 호출하는 컴파일러 드라이버를 제공한다 

1. 먼저 C 전처리기 (cpp) 를 돌리고, C 전처리기는 C 소스 파일 main.c 를 ASCII 중간 파일인 main.i 로 번역한다 

cpp [other arguments] main.c /tmp/main.i

 

2. C 컴파일러 (cc1) 을 돌려서 main.i 를 ASCII 어셈블리 언어 파일인 main.s 로 번역해 준다

cc1 /tmp/main.i -0g [other arguments] -o /tmp/main.s

 

3. 어셈블러 (as) 를 돌려서 main.s 를 재배치 가능한 바이너리 목적파일인 main.o 로 번역한다 

as [other arguments] -o /tmp/main.o /tmp/main.s

 

4. 링커 프로그램 ld 를 실행해 필요한 시스템 목적파일들과 함께 실행 가능 목적파일 prog 를 생성하기 위해 main.o 와 sum.o 를 연결한다 

ld -o prog [systm object files and args] /tmp/main.o /tmp.sum.o

-> C 소스 파일은 전처리기, 컴파일러, 어셈블러를 거쳐 목적 파일로 변환되고, 링커를 통해 여러 목적 파일이 연결되어 실행 가능한 목적 파일로 생성된다 

 

쉘은 로더 Loader 라고 부르는 운영체제 내의 함수를 호출하며

로더는 prog 의 코드와 데이터를 메모리로 복사하고, 제어를 프로그램의 시작 부분으로 전환한다 

+ 쉘 Shell 은 사용자와 운영체제 간의 인터페이스 역할을 하는 프로그램

사용자가 실행할 프로그램을 입력하면, 쉘은 운영체제 내에서 로더를 호출하여 프로그램을 실행한다 

로더 Loader 는 프로그램을 메모리에 적재하고 실행하는 역할을 한다 

prog 라는 프로그램의 코드와 데이터를 메모리로 복사하고, 제어를 프로그램의 시작 부분으로 전환하여 프로그램을 실행한다 

7.2 정적 연결 

정적 링커들은 재배치 가능한 목적파일들과 명령줄 인자들을 입력으로 받아들여서 로드될 수 있고

실행될 수 있는 완전히 링크된 실행 가능 목적파일을 출력으로 생성한다 

실행파일을 만들기 위해서 링커는 두 가지 주요 작업을 수행해야 한다

1단계, 심볼 해석 Symbol Resolution 

목적파일들은 심볼들을 정의하고 참조하며, 각 심볼은 함수, 전열 변수 또는 정적 변수에 대응된다

심볼 해석의 목적은 각각의 심볼 참조를 정확하게 하나의 심볼 정의에 연결하는 것이다 

+ 심볼의 정의와 참조를 해결해 주어야 프로그램이 올바르게 동작하게 된다 

2단계, 재배치 Relocation 

컴파일러와 어셈블러는 주소 0번지에서 시작하는 코드와 데이터 섹션들을 생성한다

링커는 이 섹션들을 각 심볼 정의와 연결시켜서 재배치하며,

이 심볼들로 가는 모든 참조들을 수정해서 이들이 이 메모리 위치를 가리키도록 한다 

링커는 '재배치 엔트리'에 따라 사용자 모르게 이러한 재배치 작업을 수행한다 

+ 컴파일러와 어셈블러에서 생성된 코드와 데이터 섹션들은 주소 0번지에서 시작하는 것처럼 작성이 되고 

링커는 여러 목적 파일 (Object File) 들을 하나로 묶는 과정에서 실제 메모리 주소에 맞게 재배치하고

각 심볼이 실제로 메모리에서 어디에 위치할지를 결정하고 그 위치를 가리키도록 모든 참조를 수정한다 

즉, 링커는 심볼을 올바르게 연결하고 재배치하여 컴파일된 프로그램이 실제 메모리에서 올바르게 동작할 수 있도록 만들어준다 

 

목적파일들은 단지 바이트 블록들의 집합이다, 이 블록들 중 일부는 프로그램 코드를 포함하고, 다른 블록은 프로그램 데이터를,

또 다른 블록들은 링커와 로더를 안내하는 데이터 구조를 포함한다 

링커는 블록들을 함께 연결하고 이 연결된 블록들을 위한 런타임 위치를 결정하며, 코드와 데이터 블록 내에 여러 가지 위치를 수정한다 

7.3 목적 파일

목적 파일에는 세 가지 형태가 있다

재배치 가능 목적파일 Relocatable Object File 

포맷에 컴파일 할 때 실행 가능 목적파일을 생성하기 위해 다른 재구성가능 목적파일들과 결합될 수 있는 바이너리 코드와 데이터를 포함한다

실행 가능 목적파일 Executable Object File 

메모리에 직접 복사될 수 있고 실행될 수 있는 형태로 바이너리 코드와 데이터를 포함한다

공유 목적파일 Shared Object File 

로드타임 또는 런타임 시에 동적으로 링크되고 메모리에 로드될 수 있는 특수한 유형의 재배치 가능 목적파일이다 

 

컴파일러와 어셈블러는 재배치 가능 목적파일을 생성하고 링커는 실행 가능한 목적파일을 생성한다

기술적으로 하하나의 목적 모듈은 바이트의 배열이며, 목적파일은 디스크에 파일로 저장된 목적 모듈이다 

 

7.4 재배치 가능 목적파일 

ELF 헤더는 이 파일을 생성한 워드 크기와 시스템의 바이트 순서를 나타내는 16바이트 배열로 시작하여 

링커가 목적파일을 구문분석하고 해석하도록 하는 정보를 포함한다 ex. ELF 헤더 크기, 목적파일 타입 (예, 재배치 가능, 실행 가능, 공유), 머신 타입 (예, x86-64), 섹션 헤더 테이블의 파일 오프셋, 섹션 헤더 테이블의 크기와 엔트리 수 등

+ ELF 헤더 Executable and Linkable Format Header 는 ELF 파일이 어떤 형식으로 작성되었고 어떻게 해석해야 하는지를 운영체제와 로더에게 알려주는 역할을 한다 

 

전형적인 ELF 재배치 가능 목적파일은 다음과 같은 섹션들을 포함한다 

.text 컴파일된 프로그램의 머신 코드

.rodate printf 문자의 포맷 스트링, switch 문의 점프 테이블과 같은 읽기-허용 데이터 

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

.bss 초기화되지 않은 C 전역변수 및 정적변수 그리고 0으로 초기화된 전역변수 및 정적변수, 목적파일에서 실제 공간을 차지하지는 않지만 단순히 위치를 표시한다, 런타임에 이 변수들은 메모리에 0으로 초기화되어 할당된다 

.symtab 프로그램에서 정의되고 참조되는 전역변수들과 함수에 대한 정보를 가지고 있는 심볼 테이블, 모든 재배치 가능 목적 파일은 .symtab 에 심볼 테이블을 가지고 있다, 그러나 지역변수에 대한 엔트리를 가지고 있지 않다 + 디버깅 및 링킹 과정에서 주로 사용한다

(+ 지역변수는 함수 내부에서만 사용되기 때문에 주로 디버깅 목적으로 사용되는 .debug 섹션에 기록된다)

.rel.text 링커가 이 목적파일을 다른 파일들과 연결할 때 수정되어야 하는 .text 섹션 내 위치들의 리스트

.rel.data 이 모듈에 의해 정의되거나 참조되는 전역변수들에 대한 재배치 정보 

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

.line 최초 C 소스 프로그램과 .text 섹션 내 머신 코드 인스트럭션 내 라인 번호들 간의 매핑 (-g 옵션으로 생성)

.strtab .strtab 과 .debug 섹션들 내에 있는 심볼 테이블과 섹션 헤더들에 있는 섹션 이름들을 위한 스트링 테이블 + 심볼 테이블에서 심볼 이름을 찾거나, 섹션 헤더에서 섹션 이름을 확인할 때 .strtab 을 통해 그 이름을 가져오는 구조 

7.5 심볼과 심볼 테이블

각 재배치 가능 목적 모듈 m 은 m 에 의해서 정의되고 참조되는 심볼들에 대한 정보를 포함하는 심볼 테이블을 가지고 있다 

링커의 문맥 내에서 세 가지 서로 다른 종류의 심볼이 존재한다

1) 모듈 m 에 의해 정의되고 다른 모듈들에 의해서 참조될 수 있는 전역 심볼, 전역 링커 시볼은 비정적 C 함수와 전역 변수들에 대응된다 

2) 모듈 m 에 의해 참조되지만 다른 모듈에 의해 정의된 전역 심볼, 이러한 심볼들은 external 이라고 부르며, 다른 모듈 내에서 정의된 전역 변수들과 비정적 C 함수들에 대응된다

3) 모듈 m 에 의해서 배타적으로 참조되고 정의된 지역 심볼, 이들은 static 타입 선언으로 정의된 정적 C 함수와 전역 변수들에 대응된다, 이 심볼들은 모듈 m 내 어디에서나 접근할 수 있지만, 다른 모듈에 의해서는 참조될 수 없다 

+ 링커는 이러한 정보를 바탕으로 프로그램의 모듈들이 올바르게 연결될 수 있도록 하며, 심볼의 충돌이나 중복을 방지한다 

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

실행 가능 목적파일 prog 를 실행하기 위해서, 리눅스 쉘의 명령줄에  이름을 아래와 같이 입력할 수 있다

linux> ./prog

 

prog 가 내장 쉘 명령어에 대응되지 않기 때문에 쉘은 prog 가 실행 가능한 목적파일이라고 가정되며,

쉘은 로더 Loader 라고 알려진 메모리 상주 운영체제 코드를 호출해서 이 프로그램을 실행한다 

 

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

로더는 디스크로부터 실행 가능한 목적파일 내의 코드와 데이터를 메모리로 복사하고

이 프로그램의 첫 번째 인스트럭션, 즉 엔트리 포인터로 점프해서 프로그램을 실행한다

이렇게 프로그램을 메모리로 복사하고 실행하는 과정을 로딩 Loading 이라고 부른다 

 

CH07 에서 제일 중요한 그림

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

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

이 다음에는 공유 모듈들을 위해 예약된 영역이 존재하고, 사용자 스택은 가장 큰 합법적 사용자 주소 아래에서 시작해서 더 작은 메모리 주소 방향 (아래) 로 성장한다, 스택 위의 영역은 운영체제 메모리 상주 부분인 커널의 코드와 데이터를 위해 예약되어 있다 

+ 위에서 볼 때 커널 메모리/ 사용자 스택/ 공유 모듈 예약된 영역/ heap 영역/ 데이터 세그먼트/ 코드 세그먼트 순

1. 코드 세그먼트 Text Segment - 프로그램의 실행 가능한 코드가 저장되는 영역 

2. 데이터 세그먼트 Data Segment - 초기화된 전역 변수와 정적 변수가 저장되는 영역 

3. 힙 Heap - 동적으로 할당되는 메모리 영역 

4. 공유 모듈 예약 영역 Shared Libraries - 동적으로 로드된 공유 라이브러리들이 사용되는 영역 

5. 사용자 스택 Stack - 함수 호출 시 사용되는 스택 프레임이 저장되는 영역 

6. 커널 메모리 Kernal Memory - 사용자 공간과 커널 공간이 분리되어 있으며, 커널 코드와 데이터를 위한 영역, 사용자 프로그램이 이 영역에 접근하면 메모리 보호 오류 Segmentation Fault 발생 

이 구조 덕분에 프로그램은 메모리 자원을 효율적으로 사용할 수 있으며, 각 영역이 서로 침범하지 않도록 운영체제에서 관리된다 

 

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

이들 영역의 위치가 매번 변경될지라도 상대적인 위치는 동일하다 

+ ASLR 은 프로그램의 보안성을 높이기 위해 사용되는 기법, 영역이 무작위로 배치되도록 만들어서 공격자가 메모리 주소를 예측하기 어렵게 만든다, 메모리 취약점을 이용한 공격을 방어하는 데 효과적 

 

로더가 돌아갈 때 실행파일 내부의 프로그램 헤더 테이블에 따라 실행파일의 덩어리를 코드와 데이터 세그먼트로 복사하고

프로그램의 엔트리 포인트로 점프하며, 이것은 항상 _start 함수의 주소가 된다

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

_start 함수는 시스템 초기화 함수인 __libc_start_main 을 호출하며, 이 함수는 실행 환경을 초기화하고, 

사용자 수준의 main 함수를 호출하고, 리턴 값을 처리하며, 필요한 경우 제어권을 커널로 넘겨준다 

+ 로더는 실행 파일을 메모리에 로드하는 역할을 한다, 실행 파일의 프로그램 헤더 테이블을 참고하여, 실행 파일의 특정 덩어리 (섹션) 를 적절한 메모리 주소에 복사하고 이 과정에서 코드 세그먼트와 데이터 세그먼트로 프로그램이 나뉘어 로드된다 

_start 함수는 C 런타임 시작 코드로, 시스템 초기화와 사용자 프로그램의 main() 함수를 호출하는 역할을 한다 

__libc_start_main 함수는 프로그램의 실행 환경 (전역 변수, 스택, 힙 등) 을 초기화하고 사용자 수준의 main() 함수를 호출하여 실제 프로그램 실행을 시작한다, 그리고 main() 함수의 리턴 값을 처리하고, 프로그램이 종료되면 제어를 커널에 넘겨 종료한다 

 

+추가

1. 사용자 프로그램의 main() 함수 호출 (_start 함수 실행)

특정 프로그램의 진입점인 main() 함수를 직접적으로 호출하는 것을 의미, 프로그램이 사용자 정의 로직을 실행하기 시작하는 지점 

2. 사용자 수준의 main 함수 호출 (__libc_start_main 함수 실행) 

사용자 프로그램이 아닌 시스템 또는 라이브러리 코드에서의 호출을 강조한다, 사용자 수준이라는 표현은 커널의 코드와 구분되는, 사용자가 작성한 코드를 의미한다 

_start 함수는 프로그램이 실행될 때 초기화를 수행하는 시스템 수준의 함수로, 실제 사용자 코드가 실행되기 전에 필요한 설정을 완료하고, 사용자 수준의 main 함수는 _start 함수의 준비가 끝난 후에 호출되는 사용자 코드로, 프로그램의 실제 동작이 이루어지는 부분이다 

결론은 _start 는 준비 단계이고, 사용자 수준의 main() 함수는 실행 단계라고 이해하면 된다 

반응형