Krafton_Jungle/Study

Krafton_jungle 5기 5주차 WIL - Linker

전낙타 2024. 4. 23. 17:00

링커

링킹(linking) 여러개의 코드와 데이터를 모아서 한 개의 파일로 만드는 작업.
로드타임, 실행시에도 수행될 수 있고 독립적인 컴파일을 가능하게 한다.
모듈을 나누고 하나의 모듈만 재 컴파일 하는 방식으로 사용된다.
링커를 이해해야 하는 이유 링커를 이해하면 큰 프로그램을 작성하는데 도움이 된다.
특히 맞지않는 라이브러리 버전때문에 링커 에러가 발생하는 경우는 링커가 참조를 해결해나가는 과정을 이해하고 있어야 해결할 수 있다.
위험한 프로그래밍 에러를 피할 수 있다. 전역변수를 중복해서 정의한 프로그램도 기본 설정의 경우 경고 메시지 없이 링커를 통과할 수 있다. 이런 에러를 예방하려면 링커를 이해해야 함.
링킹을 이해하면 어떻게 언어의 변수 영역 규칙이 구현되는지 이해할 수 있다.

 

컴파일러 드라이버

GNU GNU는 "GNU's Not Unix!"의 재귀적 약자로, 리처드 스톨만(Richard Stallman)이 1983년에 시작한 프로젝트. 이 프로젝트는 여러 가지 자유 소프트웨어 도구와 운영 체제를 개발하기 위해 시작되었음
GCC GCC는 GNU 프로젝트의 일환으로 개발된 오픈소스 컴파일러 컬렉션이다.
다양한 프로그래밍 언어를 컴파일 할 수 있으며, C, C++, Fortran, Ada, GO 등 다양한 언어로 작성된 소스 코드를 기계 코드로 변환하는 역할을 한다
컴파일 과정은 .c -> 전처리기 -> .i -> 컴파일러 -> .s -> 어셈블러 -> .o -> 링커 -> .h 로 구성되어 있다
명령어 gcc -Og -o prog main.c sum.c
위 명령어는 모든 컴파일 과정을 마치고 바로 실행파일을 생성하는 명령어이다.
해당 과정을 하나하나 뜯어보면 다음과 같다.

cpp [other arguments] main.c /tmp/main.i
먼저 C 전처리기를 돌려서 main.c 파일을 main.i 파일로 변환한다.

cc1 /tmp/main.i -Og [other arguments] -o /tmp/main.s
다음으로 C 컴파일러를 돌려서 main.i를 ASCII 어셈블리 언어 파일은 main.s파일로 번역해준다.

as [other arguments] -o /tmp/main.o /tmp/main.s
그 다음에 드라이버는 어셈블러를 돌려서 main을 재배치 가능한 바이너리 목적파일로 번역한다.

ld -o prog [system object files and args] /tmp/main.o /tmp/sum.o
마지막으로 링커 프로그램을 실행하는데, 이것은 필요한 시스템 목적파일들과 함께 실행 가능 목적파일 prog를 생성하기 위해 main.o와 sum.o를 연결한다.

 

재배치 가능 목적파일

ELF 목적파일 위 그림은 ELF 재배치 가능 목적파일의 포맷을 보여준다. ELF 헤더는 이 파일을 생성한 워드 크기와 시스템 바이트 순서를 나타내는 16바이트 배열로 시작한다.
ELF 헤더의 나머지는 링커가 목적파일을 구문 분석하고 해석하도록 하는 정보를 포함하고 있다.
  • 파일의 유형(재배치 가능, 실행 가능, 공유 라이브러리 등)
  • 기계 아키텍처 타입(x86, ARM, MIPS 등)
  • ELF 파일의 버전
  • 프로그램의 진입점 주소
  • 섹션 헤더 테이블과 프로그램 헤더 테이블의 오프셋과 크기 등
바이너리 목적파일을 역 어셈블링 해보면 확인할 수 있는데 그 예시는 다음과 같다.
section .data
ehdr:
    db      0x7F, 'E', 'L', 'F'      ; ELF 식별자
    db      2                           ; 64비트 객체 파일
    db      1                           ; 리틀 엔디안 바이트 순서
    db      1                           ; ELF 버전
    db      0                           ; 타겟 ABI (시스템의 기본)
    times 8 db 0                        ; 패딩

section .text
global _start

_start:
    ; ELF 헤더 필드 설정
    mov     rsi, ehdr                   ; rsi 레지스터에 ELF 헤더 주소 저장
    mov     rdi, 1                      ; 파일 디스크립터 1 (표준 출력)
    mov     rdx, 16                     ; 전체 ELF 헤더 크기
    mov     rax, 1                      ; sys_write 시스템 콜 번호
    syscall                             ; 시스템 콜 실행

    ; 여기서 추가적인 ELF 헤더 설정 및 프로그램 로직을 구현할 수 있습니다.

다음과 같이 해당 바이너리 파일의 메타데이터와 구조를 정의하는 포맷으로 이해하면 된다.

 

ELF 섹션 내용
  1. .text: 코드 섹션으로, 프로그램의 기계 코드 명령어가 포함.
  2. .data: 초기화된 전역 및 정적 변수를 저장하는 데이터 섹션.
  3. .bss: 초기화되지 않은 전역 및 정적 변수를 위한 섹션. 실제 실행 파일에서는 메모리 공간을 차지하지 않지만, 프로그램 실행 시 필요한 공간을 할당.
  4. .rodata: 읽기 전용 데이터를 저장하는 섹션으로, 코드에서 읽기 전용으로 사용되는 문자열이나 상수 등이 저장.
  5. .symtab: 심볼 테이블 섹션으로, 파일 내의 심볼 정보(변수, 함수 이름 등)를 저장.
  6. .rel.text, .rel.data: 재배치 정보를 저장하는 섹션으로, .text 및 .data 섹션에서 사용되는 재배치가 필요한 심볼과 그에 대한 정보를 포함.
  7. .debug: 디버깅 정보를 저장하는 섹션으로, 프로그램의 디버깅 정보가 저장

 

 

실행 가능 목적파일의 로딩

실행파일 실행가능 목적파일 rbtree를 실행하기 위해서, 리눅스 쉘의 명령줄에 그 이름을 위 사진과 같이 입력할 수 있다.
rbtree가 내장 쉘 명령어에 대응되지 않기 때문에 쉘은 prog가 실행 가능한 목적 파일이라고 가정되며, 쉘은 로더라고 알려진 메모리 상주 운영체제 코드를 호출해서 이 프로그램을 실행한다.
Loader 모든 리눅스 프로그램은 execve 함수를 호출해서 로더를 호출할 수 있다.
로더는 디스크로부터 인스트럭션, 즉 엔트리 포인트로 점프해서 프로그램을 실행한다.
이와 같이 프로그램을 메모리로 복사하고 실행하는 과정을 로딩이라고 부른다.

 

런타임 메모리 모든 실행중인 리눅스 프로그램은 위 그림과 유사한 런타임 메모리 이미지를 가진다.
x86-64 리눅스 시스템에서 코드 세그먼트는 주소 0x400000에서 시작하고, 뒤이어 데이터 세그먼트가 온다.
런타임 힙은 데이터 세그먼트 다음에 따라오고 , malloc 라이브러리를 호출해서 위로 성장한다.
이 다음에는 공유 모듈들을 위해 예약된 영역이 존재한다.
사용자 스택은 가장 큰 합법적 사용자 주소 아래에서 시작해서 더 작은 메모리 주소 방향인 아래로 성장한다. (최초 스택 범위 지정).
스택 위의 영역은 운영체제의 메모리 상주 부분인 커널의 고드와 데이터를 위해 예약되어 있다.
코드 세그먼트 코드 세그먼트는 실행 가능한 프로그램의 명령어들이 저장되는 메모리 영역.
이 세그먼트에는 프로그램의 실제 실행 로직이 포함되어 있으며, CPU가 해석하여 실행하는 기계 코드 명령어들로 구성된다.
예를 들어, 프로그램이 수행해야 할 작업을 나타내는 함수나 메서드의 구현, 제어 흐름을 제어하는 조건문(if, else), 반복문(for, while), 루프, 호출되는 다른 함수들의 호출 지시 등이 코드 세그먼트에 저장된다.
ELF파일의 .text 섹션은 이 코드 세크먼트에 해당됨
그동안 stack에 제어문 관련 함수가 저장되어 자체적으로 연산을 실행한다고 생각했었는데 그게 아닌 코드 세그먼트에 저장되어있는 함수를 stack 호출하는 방식으로 프로시저가 실행되는거였다.
데이터 세그먼트 데이터 세그먼트는 프로그램의 데이터를 저장하는 메모리 영역이다.
이 영역에는 초기화된 전역변수, 정적변수, 배열, 문자열등의 데이터가 저장된다..
데이터 세그먼트는 크게 두 가지 부분으로 나뉜다.
1. data 섹션
이 섹션에서는 초기화된 전역변수와 정적변수가 저장된다. 초기화된 변수는 프로그램이 시작될 때 해당 값으로 초기화되며, 프로그램 실행 중에도 이 값을 변경할 수 있다.
2. bss 섹션
이 섹션에는 초기값이 0인 전역변수와 정적 변수, 즉 초기화되지 않은 전역 변수와 정적 변수가 저장된다.
이 변수들은 프로그램이 실행될 때 메모리에 할당되지만, 초기화 되지 않은 상태로 남아있는다.
자바의 String pool에 문자열이 프로그램의 상수나 리터럴로 선언되었다면 해당 문자열은 데이터 세그먼트의 .rodata 섹션에 저장될 수 있다.
따라서 new String으로 선언해주지 않은 문자열은 데이터 세그먼트에 선언되어 사실상 전역변수와 같은 역할을 수행하는것