실행파일이 만들어지는 과정 - Ubuntu Linux

2021. 5. 16. 21:33Layer7/Reverse Engineering

728x90

- 리눅스란 무엇일까?

 

리눅스는 윈도우와 같은 OS의 한 종류입니다.

 

차이점이 있다면 윈도우는 사용자 친화적인 OS이고, 리눅스는 컴퓨터 친화적인 OS입니다.

 

윈도우는 굉장히 간편한 UI와 사용법이 특징이기에 사용자 친화적인 OS라 하는 것이고,

 

리눅스는 UI가 복잡하며, 거의 모든 것을 명령어로 해결해야한다고 봐도 무방합니다.

또한, 리눅스는 어셈블리 언어로 되어있기 때문에 윈도우보다 더욱 세부적인 내용을 다룰 수 있습니다.

 

 

- 리눅스와 윈도우에서의 C 소스코드 컴파일

 

컴파일러나 환경, 앱 종류에 따라 달라지겠지만 윈도우의 Dev C++을 기준으로 설명하겠습니다.

 

우리가 소스코드를 작성하고 실행을 하려면 컴파일이라는 과정을 거쳐야 합니다.

그리고 컴파일을 하기 위해선 컴파일러가 필요합니다.

또한, 컴파일러는 종류가 아주 다양하죠.

 

어떤 컴파일러로 컴파일 할지는 클릭 몇 번으로 정할 수 있고,

F11만 누르면 컴파일이 되는 편리한 운영체제가 바로 윈도우입니다.

 

리눅스는 어떤 컴파일러를 쓸지, 어떤 옵션으로 컴파일을 할지, 어떤 소스코드를 컴파일 할지 등등

모든 것을 전부 콘솔창에서 명령어로 수행해야 합니다.

 

이 글을 읽고 계시는 분들이 컴파일과 컴파일러의 대략적인 정의는 알 것이라 믿고

컴파일러가 정확히 뭔지는 아래에서 설명하겠습니다.

 

 

- vim 명령어로 소스코드 만들기

 

$ gcc --save-temps ./test.c -o ./test 

 

test라는 C코드를 생성하는 명령입니다.

 

이 명령어를 실행하고 나면 test test.c test.i test.o test.s 라는 5개의 파일이 생성됩니다.

 

위의 파일들은 모두 test라는 C파일을 구성하고 있는 파일들입니다.

 

test.c : C 소스코드가 담긴 파일 (소스파일)

test.i : *전처리기를 거친 C 소스코드가 담긴 파일 (소스파일2)

test.o : *어셈블러를 이용해 *어셈블리어를 기계어로 컴파일한 파일 (Object file, 목적파일)

test.s : 컴파일러를 이용해 소스코드를 어셈블리어로 컴파일한 파일 (어셈파일)

test : 목적파일을 *링커로 실행 가능하게 만든 파일 (실행파일)

 

* #으로 시작하는 전처리 지시자의 지시(헤더파일, define 등등)에 따라 소스코드를 변경해준다.

* 어셈블리어를 기계어(바이너리 코드, 이진수)로 바꿔준다.

* 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어

* 프로그램 내부의 참조 함수, 라이브러리 등을 하나로 묶고, 연결해준다.

 

 

- 전처리 지시자란?

 

전처리 지시자는 컴파일러가 소스파일을 컴파일 하기 전에 처리되는 작업을 나타내는 키워드입니다.

C/C++에서 앞에 #이 붙으면 전처리 지시자라고 보면 됩니다.

 

전처리 지시자의 종류는 아래와 같습니다.

 

#include, #define, #if, #ifdef, #line, #error, #program 등등....

 

자세한 설명은 생략하겠습니다.

 

 

- 컴파일러와 컴파일이란?

 

컴파일러란 하나의 프로그래밍 언어로 짜여진 프로그램을 다른 언어로 번환해주는

컴퓨터 프로그램을 뜻합니다.

 

실행 프로그램인 .exe를 만들기 위해서는 어셈블리어와 같은 저급 언어가 필요합니다.

그렇기에 C/C++같은 고급언어에서 저급언어로 바꿔주는 과정에 자주 사용됩니다.

 

컴파일러의 작동 원리는 아래와 같습니다.

 

낱말 분석 -> 구문 분석 -> 의미 분석 -> 중간 표현(IR)생성 ->코드 최적화(독립) -> 코드 생성 ->코드 최적화(의존)

 

  • 낱말 분석, 구문 분석, 의미 분석 : 각 소스코드의 의미에 대해 알아가는 과정입니다.
  • 중간 표현(IR)생성, 코드 최적화(독립) : CPU에 독립적인 최적화를 위해 중간 코드를 만들고 최적화 합니다.
  • 코드 생성, 코드 최적화(의존) : 어셈블리어로 바꾸고, CPU에 의존적인 최적화를 진행

 

이 과정에서 나온 것이 바로 어셈블리어로 이루어진 코드 파일입니다.

 

 

- 어셈블러와 링커

 

이제 어셈블러를 통해 이 어셈블리어를 바이너리 코드로 바꿔서 목적 파일을 만들어줘야 합니다.

어셈블러의 정의에 관한 것은 위에서 설명했기 때문에 부가 설명은 하지 않겠습니다.

 

어셈블리어들은 각 *ISA의 명령어 포멧에 따라 바이너리 코드로 변하며 목적 파일을 생성해줍니다.

 

* 명령어 집합 구조

 

어셈블러로 목적 파일까지 만들어줬으니 이제 실행이 가능하겠죠?

 

...라고 생각하면 오산입니다. 그것도 한참.

가장 중요한 과정인 마무리 단계, 링킹 과정이 빠졌기 때문이죠.

 

링커 역시 정의는 위에서 설명 했기 때문에 생략하겠습니다.

 

링킹을 할 때에는 두 가지 경우가 생깁니다.

 

  • 한 가지 목적 파일에 소수의 목적 파일과 라이브러리가 링킹 되는 경우
  • 한 가지/한 가지 이상 목적 파일에 다수의 목적 파일과 라이브러리가 링킹 되는 경우

 

첫 번째 경우에는 단순히 코드만 넣어주면 되는거니까 간단하게 할 수 있습니다.

 

하지만 두 번째 경우에는 코드를 넣어주는 방식으로는 할 수가 없습니다.

그러면 어떻게 해야할까요?

 

이 경우에 방법은 아래와 같이 두 가지가 있습니다.

 

  • Dynamic Link (동적 링크)
  • Static Link (정적 링크)

 

동적 링크는 실행 했을 때 실행 파일이 라이브러리와 연결 됩니다.

또한 실행 파일 크기가 작으며, 실행 시키는 컴퓨터에 파일이 없으면 실행이 불가합니다.

+) 프로세스 메모리에 라이브러리가 들어가게 됩니다.

 

정적 링크는 동적 링크와는 다르게 라이브러리가 통째로 목적 파일과 링크 됩니다.

실행 파일의 크기는 매우 크며, 실행 시키는 컴퓨터에 파일이 없어도 실행이 가능합니다.

+) 프로세스 메모리에 별다른 라이브러리를 로드하지 않습니다.

 

 

- 정리하자면?

 

실행 파일이 만들어지는 과정

test라는 C언어 기반 파일을 만들었을 때
test, test.c, test.i, test.s, test.o 이렇게 파일 다섯 개가 생성됩니다.
이 파일들은 test.c -> test.i -> test.s -> test.o -> test의 순으로 변환됩니다.

test.c는 소스코드, test.i는 전처리기를 거친 소스코드,
test.s는 어셈블리어가 담긴 소스코드, test.o는 바이너리 코드가 담긴 소스코드입니다.

이 코드들은 차례대로 전처리기, 컴파일러, 어셈블러, 링커를 거치며 변환됩니다.


- 디컴파일러, 디스어셈블러

디컴파일러와 디스어셈블러는 리버싱을 할 때 실행 파일만 주어진 경우에 아주 유용하게 쓰입니다.

실행 파일만 주어졌을 경우 그 하위 단계에 있는 나머지 파일들은 무엇인지 알지 못합니다.
그럴 때 디컴파일러와 디스어셈블러를 사용하여
컴파일러를 사용하기 전단계, 어셈블러를 사용하기 전단계로 돌아 갈 수 있습니다.
(~.i 파일, ~.s 파일 획득 가능)

참고로 디컴파일러와 디스어셈블러는 각 ISA마다 다릅니다.

그 이유는 무엇일까요?

바로 ISA별로 바이너리 코드 구조가 다르기 때문입니다.

728x90