프로그램이 실행되려면 소스 코드가 컴파일러에서 컴파일되고, 어셈블러가 기계어로 바꾼 다음 링커에서 실행 가능한 파일로 만들어야 한다. 그 중에서 링킹 과정에 대해서 자세히 알아보자.
링커는 하나의 실행 가능한 파일로 만들어주는 일이다. 아래 명령어를 통해 컴파일을 하면 아래 그림과 같은 과정으로 실행 가능한 형태로 만들어 준다. 링커에는 relocatable object file이 input으로 들어가고 링커에서 executable object file로 변경되어 나온다.
gcc -Og -o prog main.c sum.c
./prog
링커에서 나온 executable object file은 메모리에 올라간다. 즉 메모리의 일부에 locate되고 실행할 수 있는 형태인 것이다. linking 과정은 linking time에 따라 나눌 수 있는데, static linking은 executable object file을 만드는 compile time에 모든 과정이 이루어진다. symbol resolution, relocation 과정을 거치는데, symbol resolution은 하나의 symbol을 인식하는 과정이고, relocation은 text section, data section으로 분류된 symbol들을 메모리에 재배치하여 excutable object file을 만드는 과정이다. 반대로 dynamic linking은 Load time, run time에 linking이 모두 이루어진다.
링커를 사용하면 좋은 점이 무엇일까? 먼저, Modularity를 보장해준다. 프로그램은 큰 모듈 덩어리가 아니라 작은 소스 파일들의 결합으로 만들어지는데, 일반적인 함수로 만들어 쉽게 사용할 수 있다. 예를 들어 C언어의 수학 함수를 사용할 수 있는 라이브러리나 standard C library를 말한다. 두번째로 Efficiency이다. 여러 개의 소스 파일들을 동시에 컴파일해서 시간을 줄일 수 있고 일반적으로 많이 사용하는 함수들은 라이브러리처럼 끌어당겨 사용할 수 있다.
앞서 설명한 것처럼 링커는 symbol resolution, relocation 과정을 통해 executable object file을 만든다. symbol resolution에는 함수와 변수를 구분해서 symbol table에 저장한다. relocation 단계에서 코드와 데이터 섹션을 분리해 하나의 섹션으로 넣는다. 그래서 이 symbol들은 .o file에서 상대적 주소를 가지고 있다가 가상 메모리에서 절대 주소를 갖게 된다.
그래서 이 과정 중의 파일들은 세 가지로 분류할 수 있다.
- Relocatable Object File(.o) : linking 전의 파일 형식으로 코드와 데이터 영역이 포함된다. .c 파일은 하나의 .o 파일들을 하나씩 가진다.
- Executable Object File(.out) : linking 후 형식으로 메모리에 복사되어 실행될 코드와 데이터 영역이 포함된다.
- Shared Object File(.so)
그렇다면 Variable과 Symbol에 대해서 알아보자. 먼저, variable의 life와 scope에 대해서 알아야 한다. life는 해당 오브젝트가 메모리에 있는지 없는지로 결정되고, scope는 어떤 모듈에서 access할 수 있는지에 대한 것이다. 다른 곳에서 정의되어서 live 상태인데 visible 상태가 아닐 수 있다. 그러나 not live인데 visible 상태일 수는 없다.
- local variable : 함수 내부에서 선언된 변수로 스택 프레임에 저장된다. life는 함수가 호출되고 return 때까지 이고 함수 안에서만 access되기 때문에 function scope을 가진다. static local variable은 함수 내부에 선언되어도 stack에 올라가는게 아니라 전역 영역에 선언된다. 그래서 함수가 시작될 때 1번만 초기화되어 종료될 때까지 메모리에 상주하고 있다. 그래서 첫번째로 함수를 호출하고 return 되고 한번더 함수를 호출하면 초기화되지 않고 이전에 사용한 그대로 해당 변수를 사용할 수 있다.
- global variable : 함수 외부에서 정의된 변수들이다. 모든 프로그램에서 scope를 가지고 프로그램이 종료되면 not live 상태가 된다.
- static variable : life는 해당 함수가 호출되고 return될 때까지이지만 scope은 특정 함수, 파일에 한정된다.
Symbol은 정의된 variable이나 function의 이름을 말한다. relocatable object file의 variable이 링커에 들어가면 이름이 고정되어 symbol table에 올라가 참조된다. 링커의 입장에서는 각각의 symbol들을 아래와 같이 분류한다. 이때 variable과 다르다는 것을 기억해야 한다.
- global symbol : 현재 module에서 정의되고 다른 모듈에서 참조될 수 있는 symbol들을 말한다. 예를 들어서 C 함수 중 static이 아닌 함수들, static이 아닌 global variable을 말한다.
- external symbol : global symbol이긴 한데 현재 module에서 정의되지 않고 다른 module에서 정의된 symbol을 말한다. 다른 소스 파일에서 정의된 symbol인 경우 relocatable file에서는 external symbol은 '?'로 기록된다.
- local symbol : 현재 module에 정의되어 있으면서 현재 module에서만 참조될 수 있는 symbol이다. static 키워드와 함께 정의된 c function과 variable을 말한다. local linker symbol들은 local variable과 전혀 관계가 없다는 것을 주의해야 한다.
main 관점에서 보면, buf와 main symbol은 다른 파일에서도 access 할 수 있기 때문에 Global Symbol이다. swap()은 visible한 상태이지만 swap.c에 정의되어 있기 때문에 External Symbol이다. swap.c에서 보면, buf는 외부에서 정의되었기 때문에 external symbol이고 링킹이 되기 전까지는 '???' 상태로 유지된다. temp는 local variable로 linker가 관리하는 것이 아니라 메모리의 스택 영역에서 관리되는 변수다.
실제로 relocatable object files은 각각 text와 data 영역을 가지며 linker에 의해 가상 메모리에 할당된다.
주의해야 할 것은 바로 local symbol이다. Local Non-static C variable은 일반적인 지역변수를 말하는데 메모리의 스택 영역에 올라간다. 반면에 Local Static C variable은 .bss 혹은 .data 영역에 저장된다. Lifetime은 다르지만 Global variable과 동일하게 취급되는 것이다.
만약에 symbol이 중복된다면 symbol table에 어떻게 저장될까? 모든 symbol들은 strong과 weak로 구분할 수 있다.
- strong : Procedure과 초기화된 global symbols
- weak : 초기화되지 않은 global symbols
아래 그림처럼 같은 이름을 가진 symbol이 있다. 서로 다른 메모리 공간을 하지 않고, p2()에서 foo를 출력하면 5가 나올 것이다. 왜냐하면 초기화된 foo가 strong symbol이기 때문에 이를 기준으로 할당된다. weak symbol에 대해서는 메모리를 할당하지 않는다.
그래서 linker의 symbol rule이 있다. 먼저, 이름이 같은 strong symbol이 여러 개 잇는 경우, linker에서 에러가 발생한다. 만약에 strong symbol 하나, weak symbol 여러 개가 있다면, strong symbol을 참조하게 된다. 마지막으로 weak symbol이 여러 개 있다면 임의의 symbol이 선택된다. 첫번째 규칙 같은 경우에는 Linker의 오류가 발생하는데, 나머지 두개는 오류가 발생하지 않아서 의도치 않은 결과를 출력할 수도 있다.
예를 통해서 확인해보자.
첫 번째 예시의 경우, p1() {}이 동일하게 선언되어 오류가 발생한다.
두 번째의 경우 둘 다 weak symbol이므로 임의의 변수를 기준으로 메모리를 할당한다. 만약에 p1()에서 x = 5를 선언하고 p2()에서 x = x + 3을 했다면 p2()에서 x 값은 8이 된다. 이때 linker에 에러는 발생하지 않는다.
세 번째, 정수형 변수 2개와 double 형 변수가 선언되었다. 정수형 변수와 double형 변수가 이름이 동일하고 모두 weak symbol이다. 만약에 int x와 int y를 기준으로 할당했다고 하자. p2()에서 double x의 값을 업데이트를 하면, 8 byte가 업데이트되면서 y 값에 overwrite될 가능성이 있다.
네 번째, 정수형으로 선언된 x, y가 strong symbol이므로 p2()에서 x를 읽어오면 7을 출력하게 된다. 만약에 p2()에서 x 값을 수정하게 되면 y 값을 100% overwrite 하게 된다.
마지막으로 strong과 weak이 각각 정의된 경우라면 strong symbol이 메모리에 올라가서 p2()에서는 7 값을 읽게 된다. 큰 문제는 발생하지 않는다.
'CS > 컴퓨터구조' 카테고리의 다른 글
[컴퓨터구조] 2-3. Calling Convention (0) | 2024.12.18 |
---|---|
[컴퓨터구조] 2-2. RISC-V ISA (0) | 2024.12.01 |
[컴퓨터구조] 2-1. General Instruction Set Architecture (0) | 2024.10.23 |
[컴퓨터구조] 1. Computer Architecture (1) | 2024.10.22 |