2021. 5. 13. 20:53ㆍLayer7/Layer7_Reverse Engineering
프로그램이 만들어지는 과정
우리가 만든 프로그램을 실행할 수 있도록 하려면 여러 가지 작업을 거쳐야 한다. 우리가 손수 C언어로 작성한 소스 코드는 바로 실행할 수 없다. C언어는 고수준 언어(High-level Language)이므로 사람이 이해하기에는 쉽지만, 0과 1 밖에 구분하지 못하는 CPU는 이 C언어 코드를 가지고 어떻게 할 수가 없다. 그래서 고수준 언어를 컴퓨터 친화적인 저수준 언어(Low-level Language)로 바꾸면 드디어 우리가 만든 프로그램을 컴퓨터가 실행할 수 있게 된다.
우리가 VS에서 Ctrl+F5, 또는 Dev-C++에서 F11을 누르는 순간 우리가 작성한 C언어 코드를 기반으로 빌드가 만들어지고 실행이 된다. 그러나 우리는 빌드되는 과정에서 정확히 어떤 일이 일어나는지 잘 모른다.
test.c라는 소스 코드가 전처리되어 test.i가 되고 이는 컴파일이 되어 test.s가 되며, 이는 또한 어셈블 되어 test.o가 되고 마지막으로 링킹이라는 작업을 거쳐 test라는 실행파일이 만들어진다.
아래 사진과 같이 C언어 소스 코드를 실행파일로 만드려고 할 때 많은 파일들이 생기지만, 실행파일이 만들어진 다음에 즉시 삭제되는 임시 파일들이다.
이번 시간에 실행파일이 어떻게 만들어지는지를 자세히 알아보자.
test.c - C언어로 작성된 원시 프로그램
test.i - 전처리기를 거쳐 전처리가 완료된 파일
test.s - 전처리가 완료된 파일을 컴파일러가 어셈블리어로 변환해 준 파일
test.o - 어셈블리어로 변환된 파일을 어셈블러가 기계어로 변환해 준 목적 파일
test - 목적 파일에 마지막 작업인 링킹을 함으로써 완성된 실행 가능한 파일
전처리기
전처리
C언어 소스 코드(test.c)는 가장 먼저 전처리기를 거친다. C언어 소스 코드가 컴파일되기 전에 전처리 지시자들을 실행시켜 소스 코드를 가공하는 과정이다. 주로 소스 파일을 컴퓨터에 맞게 가공하므로 파일의 형태에 큰 변화는 없다. test.i의 형태로 전처리된 파일이 나온다.
전처리 지시자
C언어 소스 코드 위에 적혀 있는 지시자들. 예: #include, #define 등
이러한 지시자들은 C언어 코드의 컴파일에 영향을 준다. 이를 실행시켜주는 것이 전처리기이다.
#include - 특정 파일을 현재 소스 코드에 포함시키는 지시자 (stdio.h 불러올 때처럼 외부 함수 사용할 때 주로 이용)
#define - 변하지 않는 값, 상수를 지정할 수 있는 지시자
#pragma - 컴파일할 때 영향을 주는 지시자
#if, #ifdef - 어떠한 조건이 성립함에 따라 코드의 컴파일 여부를 결정하는 지시자
#error - 지정한 오류를 컴파일 과정에서 발생시켜 컴파일 오류를 발생시키는 지시자
C언어 소스 코드 (test.c) 전처리기가 내보낸 파일 (test.i)
컴파일러
컴파일
컴파일이란, 어떠한 언어를 다른 언어로 변환시키는 작업이다. 우리와 같은 상황에서는 C언어 코드를 어셈블리어로 변환할 때 컴파일이 된다고 할 수 있다.
test.i는 전처리 작업이 모두 끝난 상태라서 바로 저수준 언어인 어셈블리어(Assembly Language)로 바꿀 수 있다. 컴파일러가 C언어 코드를 어셈블리어 코드로 변환시키면 test.s 파일이 나온다.
컴파일러가 어셈블리어로 변환해준 파일 (test.s)
어셈블러
어셈블리어 (Assembly)
어셈블리어는 기계어와 1대 1 대응되는 컴퓨터 친화적(저수준) 언어이다. 오로지 0과 1만으로 구성된 기계어 그대로를 사람이 읽고 이해하기 쉽게 문자로 바꾸어 표현하는 언어이다.
각 CPU 제조사마다 ISA(명령어 구조)가 다를 수 있기 때문에 당연히 기계어도 제조사마다 다를 것이고, 자연스럽게 어셈블리어도 달라질 수밖에 없다.
어셈블
어셈블은 어셈블리어로 작성된 코드를 기계어로 변환해 주는 과정이며, 어셈블러는 어셈블을 해주는 프로그램이다.
test.s를 가지고 어셈블러는 최종적으로 기계어로 바꿔준다. 기계어는 CPU가 해석할 수 있는 언어로서, 가장 저수준이라 할 수 있다. 기계어로 바뀐 코드는 test.o라는 파일에 저장된다.
어셈블러가 어셈블 해 준 기계어 코드(목적 파일) (test.o)
링커
링커는 링킹이라는 과정을 수행하는 프로그램이다.
시대가 발전함에 따라 메모리의 용량도 커지고 컴퓨터의 성능도 좋아지다 보니 몇 십만, 백만, 심지어 몇 천만 줄짜리 소스 코드도 존재하는데, 모든 내용을 한 파일에 담아서 관리하기에는 유지보수가 어렵고 불편하기 때문에 보통 하나의 프로그램을 만들 때에는 여러 파일로 나누어서 작성한다. 여러 파일로 나누어서 작성한 C언어 코드는 앞서 설명했던 대로 전처리> 컴파일> 어셈블 과정을 거쳐서 목적 코드로 변환된다.
이때 목적 파일이 여러 개일 때 하나로 합쳐서 한 최종적인 실행파일을 만드는 것이 링커의 역할이다.
또한 링커는 라이브러리와 목적 파일을 연결시켜주는 역할도 한다.
라이브러리란, 프로그래머들이 미리 작성한 코드를 모두가 쉽고 간편하게 사용할 수 있도록 공개하여 모아둔 것이다. 우리가 원하는 로직이 이미 라이브러리의 형태로 만들어져 있으면 굳이 로직을 우리가 구현할 필요가 없고 라이브러리만 호출하면 된다.
C언어의 경우에는 stdio.h, stdlib.h, string.h, math.h, time.h 등이 대표적이다.
우리가 printf 함수를 정의한 적은 없다. 그럼에도 불구하고 사용할 수 있는 이유는 프로그램에 #include <stdio.h>와 같이 stdio.h 라이브러리를 호출했기 때문이다. 운영체제는 프로그램이 실행될 때 해당 라이브러리 파일에 정의된 printf 함수를 찾아가서 실행해 준다. 따라서 직접 정의하지 않고 라이브러리 내의 함수를 사용했을 경우에는 반드시 실행 파일이 되기 전에 목적 코드에 라이브러리 파일을 합병시켜야 한다.
그리고 목적 파일은 기계어로 되어 있기는 하지만, 아직 운영체제에서 인식할 수 없다.
따라서 목적 파일에 프로그램을 실행하기 전에 필요한 준비 작업들을 수행하고 main 함수를 호출하여 프로그램이 실행될 수 있도록 하는 startup code도 추가한다.
test.o는 목적 파일로, 우리가 작성했던 C언어가 완벽히 기계어로 변환된 파일이다. 그러나 이는 아직 실행할 수 없고 링커를 거쳐야만 완전한 실행 파일이 될 수 있다.
링킹
링커가 라이브러리와 목적 파일을 연결시켜 최종적인 실행파일을 만드는 과정이다.
정적 링킹 (Static Link)
정적 링킹은 라이브러리를 목적 파일과 합치는 링킹 방식이다. 즉, stdio.h에서 printf라는 외부 함수를 사용했을 경우 소스 파일에 stdio.h 모듈에서 printf 함수에 해당하는 코드를 목적 파일과 결합시키는 것을 정적 링킹이라 할 수 있다.
정적 링킹의 장점: 속도가 빠르고 호환성이 좋다. 실행시키는 컴퓨터에 해당 라이브러리가 없더라도 프로그램의 실행파일에 있기 때문에 어디서든 사용이 가능하다.
정적 링킹의 단점: 5개의 프로그램을 사용하는데 5개 프로그램 모두 같은 라이브러리의 코드를 똑같이 포함하고 있으면 중복이 되기 때문에 효율성이 좋지 않고 프로그램의 크기도 커진다. 그리고 라이브러리에서 변화가 생길 경우 모든 파일을 다시 컴파일을 해서 다시 링킹해야 하는 번거로운 작업을 해야 하기도 한다.
동적 링킹 (Dynamic Link)
동적 링킹은 라이브러리를 소스 파일에 포함시키지 않고 모듈의 주소만 기억하여 연결한다. 모듈이 프로세스 메모리에 적재된 다음 실행파일과 연결된다. 5개의 프로그램에서 같은 모듈을 사용했을 경우 모든 파일에 모듈을 포함시키는 정적 링킹과는 달리 모듈이 메모리에 로딩된 주소 하나만 포함시킨다. 프로그램들은 이 주소에 접근해서 라이브러리를 사용할 수 있다.
동적 링킹의 장점: 모듈의 내용을 파일에 복사한 것이 아니라 모듈의 메모리 상의 주소만 가지고 있어 프로그램의 크기가 작다. 그리고 모듈의 내용에 변화가 생기더라도 저장하고 있는 모듈의 주소는 변화된 모듈의 주소를 가리키고 있어 다시 컴파일하고 다시 링킹 하는 작업을 하지 않아도 된다.
동적 링킹의 단점: 매번 메모리에 접근해서 모듈의 내용을 불러와야하기 때문에 속도는 비교적 느리다. 그리고 실행파일이 오직 모듈의 주소만을 가지고 있기 때문에 내가 가진 컴퓨터에서 잘 동작하는 프로그램은 모듈이 설치되지 않은 다른 시스템에서 제대로 동작하지 않을 수도 있다는 호환성 문제도 있다.
정리
이번 시간에 실행파일이 만들어지는 과정에 대해서 알아보았다.
메인 사진 출처: Unsplash
© 남찬우, 2021
'Layer7 > Layer7_Reverse Engineering' 카테고리의 다른 글
리버싱 실습 (0) | 2021.05.27 |
---|---|
어셈블리 동적 분석 실습 (0) | 2021.05.24 |
어셈블리 - Assembly x64 (0) | 2021.05.18 |
Pwndbg 명령어 (0) | 2021.05.16 |
포인터에 대하여 (0) | 2021.05.10 |