| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- C
- DNS개념
- DNS해킹
- 노말틱
- bagofwords
- HEaaN
- kali
- CodeTranslation
- Normaltic
- NLP
- pynput
- 동형암호
- MachineCode
- stopword
- NLTK
- 딥러닝
- c언어
- pos tagging
- lemmatization
- Private AI
- 해킹입문
- CProgramming
- bettercap
- 비트시프트
- 15679
- 해킹 용어
- 모두의 깃&깃허브
- MITM
- AI
- youtubeNormaltic
- Today
- Total
일단 테크블로그😊
C언어가 기계어가 되는 과정을 알아보자!! 본문
0. 코드의 컴파일 과정을 알아보자
우리는 많은 프로그래밍 언어들을 사용하여 편리하게 컴퓨터에게 처리를 시킵니다. 그러나 컴퓨터는 실제로 우리가 자연어와 유사하게 사용하고 있는 '고급 언어(High-level Programming Language)'를 직접 이해할 수 없습니다. 반드시 그 언어로 작성된 소스코드가 컴퓨터가 이해할 수 있는 '저급 언어(Low-level)'로 변환해 주는 과정이 필요하죠. 이번 포스팅에서는 고급 언어인 C언어 코드가 실행 파일이 되어가는 과정을 살펴보며, 그 과정 속에서 어떻게 소스코드가 컴퓨터가 이해할 수 있는 저급 언어로 바뀌어가는지 찬찬히 살펴보고자 합니다.
1. 과정
C언어의 컴파일 과정은 대표적으로 다음과 같습니다. 우선 프로그래머가, 필요한 부분은 미리 짜여진 header 파일을 참고하여 C언어 소스코드를 작성합니다. 이를 통해 우리가 익히 아는 c언어 파일이 생성됩니다. 우리가 일반적으로 프로그램을 작성할 때는 중간과정을 거의 관여하지 않고 바로 실행파일이 생성되도록 편하게 컴파일하지만, 그 속에 숨어있는 과정을 4단계로 분류하여 알아보겠습니다.

본 포스팅에서는 GoormIDE Linux Ubuntu 가상환경 하에서 gcc 컴파일러를 사용하여 컴파일을 진행하였습니다.

(1) 전처리 과정
우선 간단한 소스코드를 작성하였습니다. 익숙하죠? 파일명은 hello.c입니다.

그 후 "gcc -E hello.c -o hello.i "명령어를 통해 hello.i 파일을 생성하였습니다. 여기서 (. i) 확장자는 전처리된 소스 코드 파일을 나타냅니다. 각각의 파라미터를 살펴보자면,
-E : 이 옵션을 사용하면 gcc를 통해 컴파일 단계로 진행하지 않고 전처리 단계만 실행할 수 있습니다.
-o : 출력 파일의 이름을 나타냅니다.
따라서 전처리기(pre-processor)에 의해 전처리만 진행된 소스코드인 hello.i를 얻을 수 있겠습니다. hello.i를 살펴보면 다음과 같습니다.

(2) 컴파일 과정
컴파일러는 전처리된 코드를 어셈블리 코드로 번역합니다. 전처리된 hello.i 파일을 컴파일을 통하여 어셈플리어 파일로 변환해 봅시다. 어셈블리어 파일의 확장자는 (. s)입니다. (. asm,. masm 등 다양한 어셈블리어 확장자가 많지만, Ubuntu 환경이므로 Unix 기반 어셈블리어 확장자인. s가 사용됩니다)

-S : 이 옵션은 gcc에게 어셈블리 코드를 생성하라고 지시하는 옵션입니다. 이 옵션에 따라 hello.i에 있는 전처리된 코드가 어셈블리 코드로 변환될 것입니다. hello.s에 들어있는 어셈블리어 코드를 살펴보시죠.

(3) 어셈블 과정
어셈블리어 파일까지 얻는 데 성공하였으나, 아직 실행파일이 되기에는 멀었습니다. 우선 어셈블리어를 기계어로 변환해 주는 어셈블러(Assembler)를 통하여 hello.s를 기계어로 변환해 봅시다. 출력 결과는 목적 파일 또는 목적 코드(Object file, Object Code)라고 불리며, 확장자는 (. o)입니다.

-c : 이 옵션을 통하여 gcc에게 컴파일 단계만 실행시킵니다. 즉, 어셈블리 코드를 목적 코드(. o)로 컴파일하고 링킹 단계는 실행하지 않게 됩니다.
hello.o를 직접 에디터로 살펴봅시다.

출력된 hello.o를 직접 확인해 보면, 글자가 깨져있음을 확인할 수 있습니다. 이는 기계어이기 때문에, 더 이상 일반적인 방법으로 살펴볼 수 없게 되었기 때문입니다. 그렇기에 우리는 tool을 이용하여 이 파일을 살펴보고자 합니다.
우선 "file *" 명령어로 hello.o 파일의 정보를 얻어봅시다. 각 정보의 의미는 다음과 같습니다.

- ELF(Executable and Linkable Format) : x86 기반 유닉스 및 유닉스 계열 시스템들의 표준 바이너리 파일 형식으로, 오프젝트 파일, 공유 라이브러리, 코어 덤프를 할 수 있게 하는 바이너리 파일이라는 뜻입니다.
- 64-bit : 바이너리가 64비트 아키텍처용으로 컴파일되었다는 뜻입니다.
- LSB : Little Endian을 채택하였습니다. 즉, 메모리에 높은 주소부터 내림차순으로 데이터를 저장한다는 뜻입니다.
- relocatable : 이 오브젝트파일이 재배치 가능하다는 뜻입니다. 즉, 아직 링킹 단계를 거치지 않았기 때문에, 링커에 의해 다른. o 파일 및 공유 라이브러리와 연결될 수 있다는 뜻입니다.
- version 1(SYSV) : ELF 파일 포맷의 버전과 변형을 뜻합니다. 여기서 SYS V는 System V 유닉스를 뜻합니다.
- not stripped : 아직 디버깅 정보나 심볼 테이블 등의 추가 정보를 제거하지 않은 상태임을 나타냅니다. 반대로"stripped"는 이러한 추가 정보가 제거된 상태를 의미합니다.
오브젝트 파일을 여는 데는 Readelf / Objdump 두 가지 툴을 사용하였습니다. Readelf는 말 그대로 ELF 파일을 read 하는 도구로써 , ELF 파일의 모든 섹션과 헤더 정보를 자세히 알 수 있습니다. Objdump는 ELF 뿐만 아니라 다양한 파일 형식의 바이너리도 지원하며, 주요 기능으로써 바이너리 코드를 어셈블리어로 disassemble 하여, 사용자가 바이너리의 기계어 코드를 직접 볼 수 있는 기능을 제공합니다. 또한 Readelf처럼 바이너리의 섹션과 헤더 정보뿐만 아니라 바이너라의 공유 라이브러리 의존성이나 재배치 정보까지 확인할 수 있습니다.
그럼 직접 오브젝트 파일을 열어서 살펴보도록 하겠습니다. "readelf -a hello.o"를 통해 hello.o를 살펴본 결과입니다. 최상단에 ELF Header에는 파일의 전반적인 구조와 특성이 알기 쉽게 나타나 있습니다.

다음은 objdump의 -d 옵션 (disassemble)을 이용하여 hello.o를 디스어셈블하여 어셈블리어로 확인해 보았습니다.

(4) 링킹 과정
링커(Linker)는 오브젝트 파일과 필요한 라이브러리 파일 등을 결합하여 최종적으로 실행 가능한 바이너리 파일을 생성합니다. 이제 오브젝트 파일을 다 확인하였으니, 마지막으로 오브젝트 파일을 링킹 하여 실제로 실행가능한 파일로 만들어 봅시다. 이번 명령어의 오브젝트 파일에는 일반적으로 우리가 컴파일할 때와 유사하게 별다른 옵션이 붙지 않습니다. 이를 통해 hello라는 실행 파일을 만들어보면, 정상적으로 작동하는 것을 확인할 수 있습니다.

hello라는 실행 파일의 정보를 살펴봅시다. hello.o의 정보에 많은 것들이 추가되어 있음이 확인됩니다. Readelf를 통하여 살펴보아도, 헤더 길이를 비롯하여 정보가 많아진 것을 확인할 수 있습니다. 이는 링킹 과정에서 다른 object 파일이나 실행에 필요하도록 라이브러리들이 첨부되면서 일어난 현상입니다.


Objdump를 통해 디스어셈블해 보아도 오브젝트 파일과는 달리 무언가 많이 첨가되어 있습니다.

이 중에서 hello.o 와 hello 실행파일 간의 main 함수 디스어셈블 결과를 비교해 봅시다.


실행 결과가 동일함을 알 수 있습니다.
마지막으로 gdb 디버거를 통하여 hello 실행파일을 디스어셈블해보면, 표현 방식이 조금 다를 뿐이지 Objdump와 동일한 결과를 나타냄을 알 수 있습니다. 여기서 주목할 점은, 아직 프로그램 상태일 때와, 실행 뒤 프로세스 상태일 때 메모리 주소가 다르다는 것입니다. 아래는 gdb 디버거를 통해 실제로 프로그램을 실행시켰을 때, 해당 프로세스가 어느 메모리 주소를 사용하고 있는지, 실제 메모리 주소를 추적할 수 있습니다.

2. 결론
이번 과정을 통하여 어떻게 C언어의 소스코드가 실행 가능한 바이너리 파일이 되는지 그 과정을 자세히 살펴보았습니다. 당연하게만 여겼던 과정을 상세히 살펴봄으로써 컴퓨터 아키텍처나 시스템 자체의 이해가 많이 늘었던 것 같습니다. 리버스 엔지니어링의 기초로써 큰 공부가 되었고, 과정 자체로도 매우 흥미로웠던 것 같습니다!
😊소중한 의견, 피드백 감사합니다!!😊