앞서 우리는 작성한 코드 덩어리가  컴파일을 통해 프로그램으로 작동됨을 확인하였다. 

그렇다면 코드들이 어떤식으로 연결되어 프로그램이 되는지 알아보자

상단의 사진은 프로그램의 빌드과정을 나타낸것으로 코드가 컴파일(기계어로 번역)하여 오브젝트 파일(파일확장자 .o)을 생성한뒤 링킹이라는 과정을 통해 프로그램이 생성됨을 확인할수 있다.


링킹 (Linking)

프로그램을 빌드하는 과정에서 이뤄지는 작업으로 멀리 떨어져있던 코드 덩어리를 하나로 연결시키는 과정이다.

우리가 자주 사용하는 printf 라는 기능은 실질적으로 우리가 작성한게 아닌 외부에서 작성한 코드를 참조해서 사용하는 방식이다.

링커 - 링킹을 해주는 프로그램으로 심볼해석 -> 재배치 순서를 거쳐 라이브러리 및 오브젝트 파일을 합치는것.

 

 

오브젝트 파일 (Objcet File, 목적 파일)

컴파일 또는 어셈블러 프로그램을 통하여 기계어로(0과 1로 이뤄진 바이너리 코드)변환된 코드로 구성된 파일을 의미한다. (파일확장자가 .obj, .o로 끝남)

윈도우는 PE(Portable executable), 리눅스는 ELF(Executable and Linking Format)으로 나뉜다.

(PE는 다른포스팅에.. ELF는 이어서 후술)

오브젝트파일의 출력형식

오브젝트 파일포멧 구성

Object File Header : 오브젝트 파일의 기초 정보를 가지고 있는 헤더 

Text Section : 기계어로 변환된 코드가 위치 

Data Section : 데이터(전역 변수, 정적 변수)가 들어 있는 부분 

Symbol Table Section : 코드에서 참조되는 심볼의 이름과 주소가 정의 되어 있는 부분 (상대주소)

  • 링커가 참조하는 심볼은 하단의 3개이다.
  • Global Symbol (전역심볼) - 전역변수 (A 파일에서 A 파일 참조)
  • External Symbol - 전역변수 (A 파일에서 B 파일 참조 EX : #include <stdio.h>)
  • Local Symbol - 지역변수 (static 변수)

Relocation Information Section : 링킹 전까지 심볼의 위치를 확정할 수 없으므로 심볼의 위치가 확정 나면 바꿔야 할 내용을 적어놓은 부분 (절대주소)

Debugging Information Secion : 디버깅에 필요한 정보가 있는 부분

 

심볼(Symbol) - 함수나 변수를 식별할때 사용하며 심볼테이블은 오브젝트 파일에서 참조되는 심볼정보(전화번호부)가 들어있다.

오브젝트 파일의 심볼테이블은 해당 오브젝트 파일의 심볼정보만 있어야하기에 다른 파일에서 참조되는 심볼의 정보는 저장이 불가능하다. (이것은 하단의 심볼간 중복 문제때문에 그렇다)

오브젝트 파일의 종류

중복문제 해결에 대한 설명전 오브젝트 파일의 종류에 대해 설명하겠다.

재배치가 가능한 오브젝트 파일
Relocatable object file
컴파일 결과로 생성되는 파일
(링킹 이전의 오브젝트 파일)
실행 가능한 오브젝트 파일
Executable object file
다른 오브젝트 파일과 합쳐져 생성된 실행파일
(링킹 이후 생성된 오브젝트 파일)
공유 오브젝트 파일 - Shared object file DLL에서 사용되는 오브젝트 파일 (확장자로 .so를 갖는다)

 

심볼해석 (Symbol Resolution) - 링커가 입력이 들어오는 재배치 가능 오브젝트 파일들의 심볼테이블 정보를 바탕으로 각 오브젝트 파일의 심볼 참조를 정확하게 하나의 심볼 정의에 연결하는 작업이다.

  • 1. 링커는 커맨드라인 입력순서대로 재배치 가능 오브젝트 파일과 아카이브 파일을 스캔한다(사진처럼 3가지로 구분하여 분리한다.)
  • 2. 링커는 파일을 스캔할때마다 재배치 가능 오브젝트 파일인지, 아카이브 파일인지 확인하고 모든 심볼 참조를 정확히 하나의 심볼정의(심볼 테이블 엔트리)에 연결한다 (만일 해석되지 않은 심볼참조가 있는경우(UNDEF에 있는걸 전부 확인한후) 에러메세지와 즉시종료한다 -> 하단의 Strong이 복수일경우)
  • 3. 심볼 테이블엔트리와 연결된경우 재배치를 진행한다.

아카이브 (archive) - 파일 전송을 위해 백업, 보관용 파일 또는 디렉토리로된 파일 (기록저장소)

심볼정의 - 해당심볼을 정의하는 심볼 테이블에 엔트리 (연결)

심볼참조 - 코드 상에서 해당심볼을 참조하는 부분

심볼의 종류

전역심볼 EX1) A에서 정의되고 B에서 참조한 심볼 non-static 함수
non-static 전역변수
EX2) A에서 참조하고 B에서 정의된 심볼
지역심볼 A에서 정의되고 A에서만 참조되는 심볼  static 함수/전역/지역변수

지역심볼의 해석

정의된 모듈 내부에서만 참조가 가능할뿐아니라 각 모듈 내부에선 지역심볼의 고유함을 보장하기 때문이다(한 파일 내부에서만 쓰니까 상관이 없다)

전역심볼의 해석

외부에서도 참조가 가능하기에 전역심볼의 참조에 대응되는 심볼정의를 현재 모듈의 심볼테이블에서 찾지 못할수도 있다 (A모듈에서 B모듈에 있는 전역심볼을 사용할경우 A모듈의 심볼테이블에선 찾을수 없다) 이러한 경우 컴파일러는 해당모듈이 오류가 아닌 다른 모듈에 정의된것을 가정하고 UNDEF(비명시됨)섹션인 심볼 테이블 엔트리를 만들어 나중에 링커가 다른 모듈의 심볼 테이블에서 찾아 심볼해석을 진행한다 (아는것부터 끝내고 모르는건 나중에 한다고 생각하면 된다)

그러나 전역심볼의 경우 여러모듈이 동일한 이름의 설정을 하는것도 가능한데 이것은 컴파일 단계에서 다른 모듈에서 동일한이름의 전역변수인지 확인할수 없기때문이다 

온전한 심볼해석을 위해선링커는 심볼해석을 위해서 전역심볼의 중복문제를 우선 처리해야한다.

 

심볼 테이블 엔트리 -> 해당 심볼이 어떤 심볼인지 확인하는 용도

컴파일러가 .s파일에 담은 심볼의 정보를 바탕으로 심볼테이블을 구성, 이를 재배치 가능 오브젝트 파일의 .symtab섹션에 저장된다. (.s파일은 컴파일러가 출력한 어셈블리어 코드 (여기선 오브젝트 파일로 읽으면 될것이다) // .S는 개발자가 직접 작성한 어셈블리어 코드)

하단의 사진은 각 엔트리의 구조를 C언어 구조체로 표현한것이다.

심볼 테이블 엔트리

이름 설명
name .strtab 섹션 내부 오프셋 저장
type Function, data저장
binding 심볼종류를 나타내며 글로벌,로컬에 저장 (Strong, weak)
section 심볼이 할당될 섹션
섹션헤더 테이블 인덱스를 저장하며 실제로 존재하지 않는 Pseudo 섹션을 가리킬수도 있다.
value 심볼의 위치저장
재배치 가능 오브젝트파일 -> 섹션내 오프셋 저장
실행파일 -> 절대 가상주소 저장
size 심볼 데이터의 바이트 단위 크기 저장

Pseudo 섹션

섹션 헤더 테이블에 엔트리가 없는 섹션을 의미하는 것으로 재배치 가능 오브젝트 파일에만 존재하고 실행파일엔 존재하지 않으며 하단의 3가지 섹션으로 나뉜다

  • 재배치가 이뤄지면 안되는 심볼인 ABS 섹션
  • 다른 모듈에 정의되있는 심볼임을 나타내는 UNDEF 섹션
  • 초기화가 되지 않은 전역변수를 위한 COMMON섹션 (심볼중복처리문제 참고)

전역 심볼 중복 문제

전역심볼은 초기화 여부에 따라 Strong 심볼과 Weak 심볼(초기화 안됨 EX : int x)로 구분되는데 Weak 심볼은 동일한 이름으로 여러번 정의될수 있는 특징이 있다. (상단의 사진 참조)

하단의 ELF파일 양식을 참고하길 바란다.

재배치 가능 오브젝트 파일(우측)

구분 설명
ELF헤더 시스템의 속성정보, 링커가 해당파일을 읽어서 분석할때 알아야하는 정보 저장 (전반적인 포맷정보)
text 컴파일된 프로그램 명령어의 기계어 코드 
rodata  .rodata - 문자열,상수, switch등 읽기 전용값
data  .data - 0이 아닌 값으로 초기화되는 전역변수 및 static변수 저장
bss .bss - 0으로 초기화 되거나 초기화 되지않는 전역변수 및 static 변수 저장
symtab .symtab - 해당 모듈에서 정의하거나 참조하는 모든 변수들이 저장된다.
rel.text .rel.text - 링킹시 필요한 .text 섹션 내 메모리 로케이션에 정보가 저장된다.
debug  .debug - 디버깅을 위한 정ㅈ보
line  원본 C 소스파일의 라인들과 .text섹션에 있는 기계어 코드들의 맵핑정보가 저장
strtab  심볼 이름들에 해당하는 문자열들과 섹션이름에 해당하는 문자열들이 저장된다.
섹션 헤더 테이블 각 섹션의 크기와 위치정보가 저장된다

컴파일러는 컴파일할때 각 심볼의 정보를 파악해 어셈블러에 전달하는데 자기모듈에서 정의되는 전역심볼의경우 Strong심볼인지, Weak심볼인지 파악하여 재배치 가능 오브젝트 파일을 만들게된다. (상단에 말했듯 해당 모듈은 이게 어떤건지 알고있으며 모듈에 없는건 일단 UNDEF에 다 넣어둔다..)

추후 링커가 링킹을 수행하는 시점에서 심볼테이블에 각 전역심볼의 유형(Strong,Weak) 정보가 담겨있기에 이 시점에서 링커가 이를 바탕으로 전역심볼 중복문제를 처리한다.

 

리눅스의 경우 하단의 3가지 방식으로 중복문제를 처리한다.

  • 동일한 이름의 Strong 심볼 여러개 -> 링커에러로 링킹종료
  • 동일한 이름의 Strong + 동일한 이름의 Weak심볼 여러개 -> Strong 심볼 선택, Weak 심볼은 전부 삭제
  • 동일한 이름의 Weak 심볼 여러개 -> 아무거나 선택 (만일 int x(A)와 char x(B)에서 개발자가 (A)를 선택함을 가정하에 49을 넣었으나 링커가 (B)에 49를 아스키코드로 판단해 1이 출력되는 문제 발생우려가 있다. 해당 문제는 컴파일상 사소한 문제일수도 있으나 실제 사용자 측면에선 큰에러로 발생할 우려가 있다 -> 선언시 초기화를 강조하는 이유)

오브젝트 파일의 .bss섹션에 넣어 표기하면 되지 않냐고 말할수도 있지만 어셈블러는 심볼테이블을 만드는 시점에서 해당 weak심볼이 링커에 선택될지 아닐지는 알수없기에 넣어둘수 없다.

따라서 어셈블러는 심볼테이블 엔트리에 COMMON센션으로 표기하고 나중에 링커는 COMMON 섹션으로 표기된 동명의 심볼을 따로 모아서 전역심볼중복문제를 처리한다.

(bss - 0으로 초기화 되거나 초기화 되지않는 전역변수 및 static 변수 저장)

중복문제를 해결할경우 각 심볼이 고유한 이름을 가지는걸 보장하므로 심볼정의를 심볼테이블에서 찾아 심볼해석을 진행할수있다.

 

컴파일 시스템은 연관된 여러 오브젝트 모듈을 단일의 LIB로 패키지화하는 기능을 제공한다. 이로인해 링킹을 수행할때 링커에 입력으로 들어가며실제로 프로그램이 참조하는 오브젝트 모듈만 실행 파일 안에 포함된다 (중복 없음)

 

정적 라이브러리 (.LIB) -  실행파일과 링크를 걸면 static하게 실행 파일 내에 포함이 되는 라이브러리로 속도는 빠르지만 라이브러리에 변화가있을경우 이와 연관된 모든 실행파일은 수정된 버전으로 재링크 및 재컴파일이 필요하다.

 

만일 LIB가 없을경우 프로그래머는 3가지 방식으로 구현이 가능하다.

  • 1. 컴파일러가 특정 함수에 대한 호출문을 발견하면 해당 함수의 코드를 직접 작성하는 방법 -> 컴파일중 함수가 없으면 새로 작성하고.. 하는 방식이나 컴파일러가 무거워진다. 
  • 2. 모든 표준함수들을 단일의 재배치 가능 오브젝트파일에 담아 공유 -> 1개의 파일에 함수를 전부넣은 헤더파일처럼 만들어 그때마다 입력하는 방식 (메모리 낭비가 너무 크고 파일에 수정사항이 생길때마다 전체를 재컴파일 해야한다.)
  • 3. 각 표준 함수별 재배치 가능 오브젝트 파일을 만들고 이를 디렉토리에 저장하는 방식 -> 사칙연산 전용, 연속게삱 전용 등의실행파일을 만들때마다 필요한 함수의 모듈을 직접 찾아 입력으로 넣는 불편함이 있다.

그러나 LIB가 있다면 필요한 함수가 퐇마된라이브러리 파일의 이름만 커맨드라인에 적으면 프로그램이 실제로 참조한 심볼이 존재하는 모듈만 복사되기에 메모리 낭비를 막을수있다. (자주 사용되는 표준함수 등의 LIB는 컴파일러 드라이버가 자동으로 입력한다.)

 

재배치 (Relocation)

링커가 복수의 Object파일을 1개의 파일로 만들경우 각 코드에서 사용하는 변수, 함수는 개별 주소(상대주소)로 구성되있는데 이것을 링커가 재배치하여 주소(절대주소)를 바꿔주는것으로 하단의 2가지 순서를 거친다.

1. 섹션 및 섹션안에 존재하는 심볼정의를 재배치한다

  • 동일 유형의 섹션들을 1개의 섹션으로 합쳐 실행파일에 담고 이것에 런타임 가상주소를 할당한다 -> 이 시점에서 모든 함수, 전역변수, Static변수는 고유한 런타임 가상주소를 갖는다 (런타임 - 프로그램 실행중)

2. 심볼 참조들을 재배치한다.

  • 심볼정의들에게 부여한 가상 주소정보를 활용해 올바른 가상주소를 가리키도록 배치한다. 여기서 재배치 가능 오브젝트파일의 .rel.text섹션과 rel.data섹션에 담기는 재배치 엔트리들에 담긴다.

실행 가능 오브젝트 파일(좌측)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 재배치 가능 오브젝트 파일(우측)

재배치 가능 오브젝트 파일과 유사한점을 찾을수 있는데 크게 다른점은 다음과 같다.

  • ELF 헤더 - 오브젝트 파일 전반적인 정보 저장, (Entry Point도 저장)
  • .text, rodata, data - 링커에 의해 최종적인 런타임 가상주소로 재배치
  • .init - _init이란 함수를 정의하며 이는 프로그램 초기화 코드에 호출되는 함수
  • 이미 재배치가 완료된 온전한 실행파일이라 .rel.text, .rel.data섹션은 미존재
  • 코드 세그먼트 - 읽기, 실행 권한이 부여된다
  • 데이터 세그먼트 - 읽기, 쓰기 권한 부여

 

재배치 엔트리

어셈블러는 심볼정의가 무엇을 가리키는지 모르지만 가리키는 심볼정의의 최종적인 가상주소를 확정할수 없는 심볼참조를 만날때마다 재배치 엔트리를 만들어 재배치 가능 오브젝트에 담고 나중에 링커를 통해 심볼참조를 어찌 수정해야하는지에 대한 정보를 담는다.

즉, 어셈블러는 해당 심볼이 어떤것을 가리켜야 한다는건 알겠는데 최종적으론 뭘 가리키는건지 모를때 재배치 엔트리라는 리스트를 만들었다가 추후 링커를 통해 재배치를 받을 리스트를 확인하고 이를 링킹한다. - .rel data, .rel.text에 있는 재배치 엔트리를 가리킨다.

 


동적링킹과 정적링킹

정적링킹 (Static Linking)

실행가능한 목적파일(응용프로그램)을 생성할때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사하는 방식 (동적링킹보단 빠른 속도를 보장)

  • 변화를 반영하기 위해선 재 컴파일하여 재링킹이 필요하다 (기존 파일(A)를 복사(B)해서 프로그램을 만들어놨는데 (A)에서 변화가 생긴경우 (B)는 구버전이 되기에 오류가 생긴다) 
  • 본인이 작성한 프로그램(A),(B)에서 외부개체(Static Library 이하 "C"라고 표현)를 사용한경우 (A),(B)는 (C)의 모든 정보를 갖고있으며 중복이 발생될 우려가 있다. (각 프로그램마다 동일한 라이브러리를 차지한다는 의미이며 메모리 크기 커지는건 덤이다)
  • 실행파일내 라이브러리 코드가 저장되기에 메모리 용량이 커진다. -> 이러한 단점으로 동적링킹이 등장하였다.
  • 정적 링킹 프로그램에서 모든 코드는 한개의 실행모듈에 담기기에 불일치에 대한 우려가 없다. (각 프로그램마다 동일한 라이브러리를 복사하더라도 실질적으로 한개의 실행모듈이 실행하기에 불일치한건 없다

정적 라이브러리 (.LIB) - 컴파일시 링커는 프로그램이 필요로하는 부분만 찾아 실행파일에 복사 -> 실행파일에 라이브러리 자체가 들어있기에 라이브러리가 필요없고 컴파일도 이뤄져있기에 컴파일시간도 단축,

 

 

동적링킹 (Dynamic Linking)

런타임시 이뤄지는 공유 라이브러리의 동적링킹

  • 실행가능한 목적파일을 생성할때 프로그램에서 사용하는 라이브러리 모듈의 주소만 갖고있다가 런타임으로 실행파일 및 라이브러리가 메모리에 적재될때 해당 주소에서 필요한 모듈만 연결(링크)하는 방식 (위치만 알고있음)
  • 기존 파일이 변경되어도 주소를 알고있기에 반영이 가능하다(단, 주소만을 갖고있기에 가볍지만 DLL이 없는경우 실행이 되다가 DLL이 필요한 시점에 파일이 없어서 에러가 나타날수 있다 -> 불일치에 대한 우려가 있다.)
  • 정적링킹과 달리 필요할때마다 라이브러리에 접근해야 하기에 오버헤드가 발생할 여지가 있다.
  • 메모리에 DLL파일이 없을경우 실행을 못하니 OS가 주소를 알고있다가 필요할때 메모리에 DLL파일을 올렸다가 사용이 끝나면 다시 내리는 방식 (자주 쓰이는 라이브러리는 메모리에 필요할때 가져오되 한개만 올리자.)
  • 가끔 프로그램 실행할떄 ".DLL파일이 없습니다!"라고 출력되는데 주로 윈도우 시스템 디렉토리에 존재한다.

동적 라이브러리 (.dll) 프로그램 실행시 실행 파일들과 연결하는 라이브러리 (프로그램이 실행될떄 필요할때만 DLL에 접근한뒤 다시 코드로 돌아오는 방식으로 실행 -> 컴파일후 실행파일과 독립되어있어 DLL파일의 위치를 실행파일에 설정한 공간에 위치시켜야한다.) OS에 의해 로드되면 물리메모리에 계속 남고 소스코드가 메모리에 올라갈때 DLL이 갖는 가상 메모리 주소와 매핑이된다.

 

구분 정적링킹 동적링킹
동작속도 빠름 느림 (접근시 오버헤드 발생)
수정사항 반영시 (재링킹 여부) 재컴파일 필요 재컴파일 필요X 
자원 사용량 많음(복사했기에 용량이 큼) 적음(주소만 알기에 작음)
불일치에 대한 우려 X (중복시 미 복사) O (있어도 일단 복사) 
오버헤드 미발생 발생(주소로 접근하는시간)
메모리 요구사항 큼(전부 복사) 적음(필요시 복사)

 

로더

HW에 있는 실행 파일의 프로그램 헤더 테이블에 적힌 정보를 바탕으로 실행파일의 연속적인 바이트 정크를 코드 세그먼트와 데이터 세그먼트에 복사한뒤 이를 메모리에 적재, 실행하는 역할

  • 컴파일 즉시 로더(Compile and Go) - 언어 번역 프로그램이 로더의 역할까지 담당하는 것으로 프로그램의 크기가 크고 한 가지 언어로만 프로그램을 작성할 수 있으나 실행을 원할 때마다 번역을 해야한다 -> 할당,재배치,적재 담당
  • 절대 로더(Absolute Loader) - 단순히 목적프로그램을 입력받아 주기억장치의 적재만 담당하는 로더 -> 가장 간단한 로더이며 프로그래머가 지정한 주소에 적재할수 있으나 한번 지정한경우 위치변경이 어렵다
  • 재배치 로더(Relocating Loader) - 주기억 장치의 상태에 따라 목적프로그램을 주기억 장치의 임의의 공간에 적재할 수 있도록 하는 로더 - 4가지 기능 모두수행
  • 링킹로더(Linking Loader) - 하나의 프로그램이 변경되어도 다른 프로그램의 재 번역이 필요 없도록 프로그램에 대한 기억장소할당과 다른 프로그램의 연결을 로더가 자동으로 수행하는 프로그램 - 직접연결로더(DLL)
  • 동적 적재(Dynamic Loading = Load on call) - 모든 세그먼트를 주기억장치에 적재하지 않고 항상 필요한 부분만 주기억장치에 적재하고 나머지는 보조기억장치에 저장해두는 기법
  • 주요기능 - 할당, 링킹, 재배치, 적재 -> 실행환경 초기화, main함수 호출 및 이에 대한 반환값 처리, 필요한경우 제어를 커널로 옮기는 역할도 수행)

 

 

링크 : https://sadfsdf.tistory.com/manage/newpost/81?type=post&returnURL=https%3A%2F%2Fsadfsdf.tistory.com%2F81

'CS' 카테고리의 다른 글

교착상태(Dead Lock)  (0) 2023.04.02
시스템콜  (0) 2023.04.01
운영체제란  (0) 2023.03.30
프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18

+ Recent posts