이전 포스트에서 Assembly 단계에서 배운 assembly instructions를 알아보았다.
엄밀히 말해서, Assembly 단계에서 기계언어로 바뀌고 이를 disassembler
의 도움을 받아 human-readable한 instructions으로 나타낼 수 있었던 것이다. Assembly 단계 이후, 분할된 object 파일을 코드들과 데이터들을 하나의 실행 가능한 파일로 만들어주는 Linking
단계를 알아보자.
링킹엔 정적링킹(static linking
)과 동적링킹(dynamic linking
) 두 가지로 나뉜다.
링킹이 동작하는 때는 compile time
, load time
(프로그램이 메모리에 할당되는 시간), run time
이다. 그렇다면 정적링킹은 compile-time, 동적링킹은 load time과 run time에 동작한다.
(-static 옵션은 정적링킹, 그 밑은 동적 링킹이다.)
컴파일에서 정적링킹을 알아보자.
직역하면 재배치 가능한 오브젝트 파일
이다. 이는 중간 형태의 코드와 데이터를 담고 있는 파일이다. 이들을 모아 하나의 실행 가능한 파일을 만든다. .o
파일은 .c
파일의 정보들만 담고 있다. 예를 들어, printf 함수를 사용할때, 이는 C언어의 내장함수로 함수의 위치를 알지 못한다. 다른 모듈에 있는 printf 파일의 주소를 찾아 실행 가능한 파일로 만드는 것이 링킹의 목표이다.
a.out
파일은 컴파일이 전부 끝난 이후에 생성되는 실행파일이다. 메모리에 바로 load되어 실행될 수 있는 형태이다.
특별한 타입
의 Relocatable object file
이다. 똑같이 링킹이 되어 실행되어야 하는 파일인건 같지만, 컴파일 단계가 아니라 run time
과 load time
에 링킹이 되어 실행될 수 있는 형태의 파일이다.
윈도우에선 DDLs
(Dynamic Link Libraries)라고 불린다.
링킹을 왜 사용할까? 간단하게 이유는 아래와 같다.
모듈화
- 링킹은 분할 컴파일을 가능하게 한다. 한 파일에 프로그래밍을 한다면 유지보수가 힘들 것이다.
- 주로 사용되는 함수를 모듈화하여 사용할 수 있다.(Math, standard C library)
효율성
- 분할 컴파일로 시간이 줄어든다. 만약 유지보수 시 전체를 컴파일한다면 시간 소모가 클 것이다.
- 사용되는 함수만 메모리에 load 할 수 있다. 모듈화의 이점이다. 링킹이 없다면 라이브러리를 통째로 load 해야 한다.
각각의 symbol reference
을 하나의 symbol definition
으로 바꾸어 준다.
같은 타입의 데이터를 한 영역에 통합시켜준다. .o 파일에 있는 타입들을 분류하여 영역에 저장한다.
모든 레퍼런스들을 symbol로 바꾸어 그들의 새로운 위치 정보를 부여한다.
Symbol과 reference를 알아보자.
symbol은 함수와 전역,지역 변수에 관한 정보를 가지고 있는 객체의 개념이다. 영어의 품사와 비슷하다고 생각하면 된다.
symbol의 역할은 아래와 같다.
- 식별자, 타입, 바인딩(전역 or 지역), 주소, 크기 등의 정보를 가지고 있다
- 컴파일러가 symbol을 object 파일에 만든다.
- 링커는 이 정보를 통해 각각의 정보와 데이터를 찾아 합병한다.
- symbol definitions는 오브젝트 파일에 저장되어 있다. 이를 symbol table이라고 부른다.
위와 같은 형태로 저장되어 있다. value는 주소 부분으로 offset이다. offset이 주소를 정하기 때문이다.
모든 Relocatable object file은 symbol table이 존재한다. 이는 .symtab 확장자 파일을 사용한다.
symbol table은 지역 데이터를 저장하지 않는다. 링킹과 지역 데이터는 관련이 없기 때문이다. 모듈화는 전역변수를 연결해주는 것이기 때문이다.
pseudosection은 실행파일에 존재하지 않는다.(질문)
val은 지역변수이기 때문에 table에 존재하지 않는다. 전역 함수들이 저장되어 있는 것을 확인할 수 있다.
Relocatable Object files
는 symbol을 정의하고 참조한다. 이게 무슨 뜻일까?
모듈화가 되어 있는 파일을 하나의 실행파일로 만드는게 링킹이라 했다. 지역변수는 모듈화에 영향을 끼치지 않는다. 그렇기 때문에 전역변수, 함수 정의부에 symbol을 부여하여 주소, 데이터 타입 등을 저장하고 이를 다른 모듈에서 사용할 경우 정의부를 찾아가 함수를 호출해야한다. 사용하는 부분
을 reference라 칭하고 선언부
를 definition이라 칭한다.
symbol의 타입은 두 가지이다. Object와 Function이다. Object
는 static, 전역 변수이다.
Function
은 static, 전역 함수이다.
Local symbols
: 지역 내에서 referenced and defined된 함수나 변수를 뜻한다. 같은 모듈 내에서이다.Global symbols
: 다른 모듈(파일)에서 referenced and defined된 함수나 변수를 뜻한다. 그럼 전역 static이면 Global symbol이 아니다. 링킹의 주업무가 이들을 연결해주는 것이다.External symbols
: Global symbol인데, 다른 모듈에서 참조되는 함수나 변수를 칭한다.
External symbol은 링킹 단계 이후 존재하지 않는다.
Section
에 대해 알아보자. 같은 종류가 저장되어 있는 인접한 공간이다.
모든 symbol이 저장되어 있다.
section index은 section의 offset이라 할 수 있다.
네 가지의 주요 section은 아래와 같다.
- .text: machine code
- .rodata: read only data
- .data: 초기화된 전역, 정적 C 변수이다.
- .bss: 초기화되지 않은 전역, 정적 C 변수이다. 초기화 하지 않아도 0으로 초기화되고, 초기화 없이
선언해도 0으로 초기화된다. .bss는 메모리에 올리지 않는다. 공간을 아끼기 위함이다.
정적, 전역 변수를 따로 저장한다. 이는 공간을 아끼기 위함이다.
.bss는 오브젝트 파일이 저장된 디스크에 할당되지 않는다. 런타임 때 메모리에 0을 가지고 할당된다.
이진수로 이루어진 오브젝트 파일
의 기준이다. 시스템마다 다르다. 리눅스에서 사용하는 ELF format
을 사용한다.
ELF format을 알아보자
ELF relocatable ojbect file
- Elf header: word size(64 or 32), file type, machine type이 저장되어 있다.
- .text: 소스코드가 저장되어 있다
- .rodata: read only data가 저장되어 있다. ex) const int
- .data: 초기화된 전역변수,함수가 저장되어 있다.
- .bss: 초기화되지 않은 전역변수,함수가 저장되어 있다. section에 저장되어 있지만 공간을 차지하지 않는다.
- .syntab: symbol table, static 변수 이름, 함수이름, section name and location이 저장되어 있다.
- .rel.text: relocation info for text 이는 링커가 해야할 일들이다. 소스코드상의 주소를 merge 단계에 바꿔야 한다는 메세지를 컴파일러가 남겨놓는다.
확실x- .rel.data: relocation info for data 포인터 변수의 주소가 저장되어 있다. 이는 실행 가능한 하나의 파일로 merge될 때 수정되어야 함으로 링커가 해야할 일이다.
확실x
symbol resolution
이란 링커가 각각의 reference
를 definition
과 연결해주는 것이다.
symbol table
을 사용하여 연결해준다.
local symbol
은 컴파일러가 고유의 식별자
를 지어준다.
extern symbol
은 다른 모듈에서 링커가 정의부를 찾아준다. 만약 링커가 중복된 정의를 발견한다면 에러 메세지를 출력한다.
global symbol
은 중복된 정의가 발견된다면 하나의 symbol을 definition으로 지정하거나 에러를 발생시킨다. 특정 규칙에 따라 둘 중 하나를 extern으로 바꿔준다.
특정 규칙은 아래와 같다.
- 함수이거나, 초기화 되어 있는 변수를
strong
이라 칭한다.- 초기화 되어 있지 않은 변수를
weak
라 칭한다.
중복되는 선언이 strong weak 관계라면 strong을 definition으로 정한다.
strong이 중복된다면 에러가 발생한다.
weak가 중복된다면 임의의로 하나를 definition으로 정한다.
만약 strong과 weak가 중복되었을 경우, strong을 definition으로 지정했을 때에도 bug가 발생할 수 있다.
위의 상황을 보았을 때 x가 중복되고 strong과 weak의 관계인 것을 확인할 수 있다. 하지만 strong 부분은 int이고 weak부분은 double이다. 이는 x를 8 bytes로 보고 int x가 저장되어 있는 공간에 8 bytes를 저장하기 때문에 int x의 공간을 벗어난 y의 공간까지 침범한 것을 확인할 수 있다.
이 부분의 value를 보자. 위에서 value는 offset 값이라고 했었다. 초기화된 전역변수의 시작은 순서대로 a,x 이다. 그렇다면 x의 주소는 .data section의 0이 된다. 그리고 두 번째의 x의 주소는 int 크기의 offset인 4 bytes가 더해진 값이 된다.
readable data인 const는 .rodata section에 저장된다.
추가로 전역변수는 사용할 필요 없으면 되도록 사용하지 않는 것이 좋다.
만약 사용해야 한다면 가능한 초기화, extern 사용, static을 사용할 수 있는지 고려해보자.
symbol resolution
단계가 끝난 이후에 Relocation
단계이다. symbol resolution 단계는 symbol table을 이용하여 reference와 definition을 연결해주는 단계였다면, Relocation은 section과 symbol definition을 통합
해주는 단계이다. 코드끼리, 데이터끼리, 함수끼리 merge 해주기 때문에 주소가 결정된다.
기존의 section들을 같은 타입(같은 section)끼리 묶어 새로운 section으로 옮긴다(new aggregate section) 그리고 run time memory 주소를 새로운 section과 symbol에 정해준다. section들은 분할된 모듈에 각각 존재한다 이들을 실행가능한 파일에 같은 타입의 section에 모아준다.
Relocation entry
란 어셈블러가 주소가 없는 symbol reference를 만났을 때 생긴다.
어셈블러가 instruction을 행해야할 reference의 메모리 주소와 소스코드상의 주소를 알지 못하기 때문에 발생한다.
이는 아직 함수나 변수들이 모듈화 되어있어 합병됐을 때의 주소를 알지 못하기 때문이다.
이들의 runtime 메모리 주소와 소스코드 상의 주소를 합병되었을 때 재부여를 해야하기 때문에
.data와 .text의 수정할 부분을 .rel.text
와 .rel.data section
에 저장된다.
이는 어셈블러
가 링커에게 수정할 부분을 section에 저장해놓는 것이다.
위의 사진을 보면 reference의 array와 sum 부분을 보자. 어셈블리어에서 array와 sum 함수의 offset 부분만 할당이 되어있다. 9와 e이다. 이걸 .rel.text와 .rel.data section에 수정해야 한다는 메세지를 어셈블리어가 남겨놓는다. 링커가 합병시 저 두 reference에 주소를 부여한다. 소스코드상의 주소(offset을 사용 끝번호를 확인할 수 있음. 둘다 9와 e이다.)와 메모리 상의 주소가 부여된 것을 확인할 수 있다.
위 부분을 보면 offset 9와 e를 확인할 수 있다. 저들이 주소를 부여받은 이후엔
4004d9와 4004de로 변경된 것을 볼 수 있다. 여전히 offset이 끝에 존재한다.