Von Neumann architecture
폰 노이만 아키텍처 개념에 따라 프로그램의 실행과정을 설명하면 다음과 같다.
- 실행 파일은 평소에 Storage에 존재하며
- 실행 시 Memory에 로드된다. (매핑)
- CPU는 이를 바탕으로 코드의 실행 흐름을 가진다.
결국 프로그램을 실행한다는 것은 CPU에게 특정 일을 시키는 것이라 할 수 있는데,
CPU는 0과 1로 이루어진 기계어만 이해할 수 있다.
Compile
그렇기 때문에 우리가 C, C++, Java, Python.. 과 같은 High Level Langauge로 작성한 소스코드를 -> 기계어로 변환해야 하며 이를 Compile이라고 한다. Compiler는 소스코드를 기계어로 번역하고 Syntax Check와 같은 과정을 거쳐 Source File을 -> Object File로 만들어 준다. (C 언어: main.c -> main.obj, Java: main.java -> main.class)
그럼 컴파일러를 통해 만들어진 Object 파일은 0과 1로 이루어진 기계 코드이므로 CPU에게 넘겨주면 실행이 바로 가능한가?
그렇지 않다.
[ Object File로 프로그램을 실행할 수 없는 이유 3가지 ]
- 파일이 로드 될 메모리 주소를 모른다.
파일이 메모리에 로드되어야 CPU가 이를 순차적으로 읽어 가며 코드의 실행 흐름을 갖는 것인데, 파일이 메모리 어디에 로드되어야 하는지에 대한 정보가 없다.
- 운영체제: 메모리 어디에 로드되어야 하는지 몰라서 로드 못함~
- CPU: (실행 못 하는 중) - 처음 실행될 코드의 주소를 모른다.
컴파일 된 파일은 1과 0으로 나열되어 있는데 첫 번째 코드, 두 번째 코드가 순차적으로 나열되어 있지 않아 어디부터 실행해야 할지 모른다. (C언어로 작성한 코드가 있다고 할 때, main 함수나 사용자 정의 함수 등이 실행흐름 순서대로 순차적으로 작성되어 있지 않을뿐더러 서로가 호출하고 참조 및 반환하는 과정이 지속되기 때문)
- 운영체제: 파일을 메모리에 로드했는데.. 처음으로 실행될 코드(entry point) 뭔데? -> 모름
- CPU: 엥 어케 실행함 - 함수, 라이브러리 정보를 포함하고 있지 않다.
Object File은 Source File을 단순히 기계코드로 변환한 것이지 함수나 라이브러리 정보를 담고 있지 않다.
- Object File: 나 printf() 함수 쓸게~
- CPU: 그게 뭔데
-> 로드된 메모리 상 어딘가에 printf() 함수의 dll 파일이 있어야 호출하여 사용할 수 있는데, 해당 함수의 코드가 있는 위치도 모를뿐더러 해당 함수를 정의하고 있는 라이브러리 정보도 없기 때문에 둘 사이의 연결해 주는 작업이 필요
Linking
위와 같은 문제 때문에 Object File을 실행가능하게 바꿔주는 과정이 추가로 필요하며 해당 과정은 Linker를 통해 이뤄진다. 컴파일러가 생성한 Object File들을 결합하여 exe와 같은 실행 파일로 만드는 과정이라 할 수 있으며 .dll 또는 .so와 같은 라이브러리의 참조를 설정한다.
DLL(Dynamic link library)
윈도우에서의 동적 라이브러리로 다른 프로그램들이 불러서 사용할 수 있는 다양한 함수들을 가지고 있다.
C:\Windows\System32 경로에서 다양한 dll 파일들을 볼 수 있다.


[+] 추가 공부: Linux에서 표준 C라이브러리 파일 확인해보기
SO(Shared Object)
.so 파일은 리눅스에서의 .dll 파일이라고 할 수 있다.
$ cd /lib/x86_64-linux-gnu
$ vim libc.so.6

바이너리 파일이므로 당연히 깨져있다.
$ readelf -h libc.so.6
redelf는 ELF format을 가진 파일 정보를 출력하는 명령어이다.
실행파일이나 object 파일의 포맷은 운영체제마다 다르다.
- ELF -> 유닉스 계열 ( .so / .a / .o / .elf )
- PE -> 윈도우 ( .dll / .exe / .sys )
(이번 리버스엔지니어링 과정에서는 PE 파일에 대해 다룰 예정)

-h 옵션으로 파일의 header만 출력하였고 파일의 다양한 정보를 담고 있는 것을 확인할 수 있다.
$ objdump -d libc.so.6 | less
objdump 명령어에 -d 옵션을 사용하면 ELF 파일을 디스어셈블하여 볼 수 있고 코드가 굉장히 많다. 일부만 보자면

대충 .plt section이고 section의 메모리주소, *ABS* (절대 주소)에서 얼만큼 떨어져 있나 offset 값, 그 아래 어셈블리 코드 등으로 구성되어 있는 것 같다.
$ readelf -s libc.so.6 | grep 'printf'
많은 코드 중에 printf 함수의 어셈블리 코드를 보고 싶어 필터링해보았다.

printf 하나만 나올 줄 알았는데 뭐가 많이 출력된다. printf 관련 함수가 여러 개 있는 것 같은데 그중에 그냥 평소에 사용하는 printf를 확인해 보면 (밑에서 세 번째 줄) 주소는 0x52b30부터 시작하고 있고 크기는 200 bytes이다.
$ objdump -d --start-address=0x52b30 --stop-address=0x52c30 libc.so.6
0x52b30부터 200바이트니까 대략 0x52c30까지 출력해 보면 아래로 쭉 코드가 출력된다.

이 아래로도 코드가 많기는 한데, 생각보다는 짧아서 보니까 아래에서 _IO_vfprintf를 call 하고 있다.

- printf -> 우리가 알고 있듯 단순 문자열을 stdout으로 터미널에 출력하는 데 사용
- vfprintf -> 출력 경로를 별도로 지정하여, 문자열 출력을 다른 파일에 할 수 있음
알고 보니 printf 함수 자체는 매개변수를 준비하는 동작만 수행하고 핵심 동작은 vfprintf를 호출하여 수행하는 형태. 그래서 주요 로직은 vfprintf 함수의 어셈블리에 있다는 말인데 아까 'printf'를 grep한 결과를 다시 보면 vfprintf는 7바이트를 사용하고 있다. 아마 vfprintf도 다른 곳의 함수를 call 하고 있는 게 아닌가.. 하는 추측
더 뜯어보고 싶은데 이렇게 하다간 26강까지 정리하는데 너무 오래 걸릴 것 같아서 일단 보류... 어셈블리 코드랑 의미 파악하는 게 익숙해지면 그때 다시 해보면 좋을 것 같다.
<정리>
- Source File -> Object File(with Copiler) -> Excutable File(with Linker)
- 컴파일러로 소스코드 -> 기계어로 변환하지만 실행은 불가능
[ 1. 메모리에 로드할 시작 주소 모름 / 2. 처음으로 실행할 코드 위치 모름 / 3. 관련 라이브러리 및 함수 정보 없음 ]
=> 그래서 파일이 실행 가능하도록 Linking 과정을 거침 - 그렇게 만들어진 실행 파일 (예를 들어 .exe) 은 Storage에 저장되어 있다가 더블클릭, 실행하는 순간 운영체제의 Loader에 의해 메모리에 로드되고 로드가 완료되면, CPU는 Entry Point부터 순차적으로 코드 실행 흐름을 가지며 프로그램이 실행된다.
'KISA > KISA - 리버스 코드 엔지니어링' 카테고리의 다른 글
| 02. 운영체제의 프로세스 가상주소공간 (0) | 2024.11.21 |
|---|