링커
링킹(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 헤더의 나머지는 링커가 목적파일을 구문 분석하고 해석하도록 하는 정보를 포함하고 있다.
|
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 섹션 내용 |
|
실행 가능 목적파일의 로딩
실행파일 | 실행가능 목적파일 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으로 선언해주지 않은 문자열은 데이터 세그먼트에 선언되어 사실상 전역변수와 같은 역할을 수행하는것 |
'Krafton_Jungle > Study' 카테고리의 다른 글
Krafton_jungle 5기 6주차 WIL - Implicit free list (0) | 2024.04.27 |
---|---|
Krafton_jungle 5기 5주차 WIL - Exceptional Control Flow (0) | 2024.04.23 |
Krafton_jungle 5기 5주차 WIL - RB_Tree (0) | 2024.04.22 |
Krafton_jungle 5기 31 ~ 32일차 TIL - RB-Tree, 알고리즘 (2) | 2024.04.19 |
Krafton_jungle 5기 30일차 TIL - C언어 기본 개념 (0) | 2024.04.16 |