카테고리 없음

리버싱 2차시 정리

sjong 2023. 6. 25. 19:31

메모리 구조

프로그램이 실행되기 위해서는 먼저 프로그램이 메모리에 로드되어야 한다.

또한, 프로그램에서 사용되는 변수들을 저장할 메모리도 필요하다.

따라서 컴퓨터의 운영체제는 프로그램의 실행을 위해 다양한 메모리 공간을 제공하고 있다.

 

메모리 공간은 다음과 같이 네 가지로 나뉜다.

메모리 공간

코드(code) 영역

메모리의 코드 영역은 실행할 프로그램의 코드가 저장되는 영역으로 텍스트 영역이라고도 부른다.

코드영역은 실행 파일을 구성하는 명령어들이 올라가는 메모리 영역으로 함수, 제어문, 상수 등이 여기에 지정된다.

CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리하게 된다.

또한 프로그램이 시작하고 종료될 때까지 메모리에 계속 남아있는다.


데이터(data) 영역

메모리의 데이터 영역은 프로그램의 전역 변수와 정적(static) 변수가 저장되는 영역이다.

데이터 영역은 프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸한다.

#include <stdio.h>

int Layer; // 초기화되지 않은 변수 영역
int Layer7 = 7; // 초기화 된 변수 영역

int main(){
	return 0;
}

힙(heap) 영역

힙 영역은 쉽게 말해서 '사용자에 의해 관리되는 영역'이다. 흔히 동적으로 할당할 변수들이 여기에 저장된다.

이 공간에 메모리 할당하는 것을 동적 할당(Dynamic Memory Allocation)이라고도 부른다.

그리고 Heap 영역은 대개 '낮은 주소에서 높은 주소', 즉 메모리 위쪽 주소부터 순서대로 할당된다.


스택(stack) 영역

스택 영역은 함수를 호출할 때 지역변수, 매개변수들이 저장되는 공간이다.

메인 함수안에서의 변수들도 이에 포함되고 함수가 종료되면 해당 함수에 할당된 변수들을 메모리에서 해제시킨다.

 

include <stdio.h>

int a = 1; // 데이터 영역에 할당
int b = 2; // 데이터 영역에 할당

int main() {
	what1(1);
    what2(2);
}

func what1(int c) {
	int d = 3; // 매개변수 c와 지역변수 d가 스택영역에 할당
}

func what2(int e) {
	int f = 4; // 매개변수 e와 지역변수 f가 스택영역에 할당
}

 

스택 영역은 푸시(push) 동작으로 데이터를 저장하고, 팝(pop) 동작으로 데이터를 인출한다.

이러한 스택은 후입선출(LIFO, Last-In First-Out) 방식에 따라 동작하므로, 가장 늦게 저장된 데이터가 가장 먼저 인출된다.

스택 영역은 메모리의 높은 주소에서 낮은 주소의 방향으로 할당된다.


위의 HEAPSTACK영역은 사실 같은 공간을 공유한다. HEAP이 메모리 위쪽 주소부터 할당되면 STACK은 아래쪽부터 할당되는 식이다. 그래서 각 영역이 상대 공간을 침범하는 일이 발생할 수 있는데 이를 각각 HEAP OVERFLOW, STACK OVERFLOW라고 칭한다.

 

Stack영역이 크면 클수록 Heap영역이 작아지고, Hea 영역이 크면 클수록 Stack영역이 작아진다.



컴파일과 어셈블

우리가 실행하는 c언어는 코드로 짜고 실행시키면 되는 단순한 구조처럼 보인다.

 

gcc로 만들 때는

 

gcc 소스파일 -o 실행파일명

ex) gcc helloworld.c  또는 gcc helloworld.c -o [옵션] helloworld

 

gcc helloworld.c 는 a.exe 실행파일

gcc helloworld.c -o [옵션] helloworld 는 helloworld.exe 라는 실행파일이 만들어진다.

 

gcc로 만들 때도 한 줄로 만들어진다 생각하지만 실제로는 아래와 같은 과정을 거친다.

보면 파일이 .c, .i, .s, .o 그리고 마지막으로 helloworld 실행파일로 바뀌는 것을 볼 수 있다.

각각의 과정을 좀 더 자세히 보자.

이처럼 빌드 과정은 여러 중간 단계를 거친다. gcc 명령어에 옵션을 추가하여 각 단계별로 결과를 확인할 수 있다.


1. 전처리(Preprocess)

 

헤더파일을 포함하고 매크로 확장을 하는 단계이다.

매크로 확장이란 헤더파일들을 찾아서 적용하는 것을 말한다.

#include <stdio.h> 
// 헤더파일
#define MAX_NUM = 10 
// 매크로

이를 진행한 뒤 결과물은 헤더파일에 정의된 변수와 함수를 포함하는 helloworld.i로 바뀌었다.

전처리기를 통해 소스코드 파일(.c)을 전처리된 소스코드 파일(.i)로 바꿔준 것이다.


2. 컴파일(Compile)

컴파일 단계에선 C언어 코드가 어셈블리어로 변환된다.

즉, 컴파일러를 통하여 전처리된 소스코드 파일(.i)를 어셈블리어 파일(.s)로 바꿔준 것이다.

 

어셈블리어는 저급언어로 기계어, 명령어라고 칭하며 이진수로 이루어져 있어 CPU명령어와 매칭된다.

 

컴파일 단계에선 단계가 좀 더 세분화되어 세 가지로 나뉜다.

  • 전단부(Front-end) : 언어 종속적인 부분 처리 - 어휘, 구문, 의미 분석
  • 중단부(Middle-end) : *SSA 기반으로 최적화 수행 - 프로그램 수행 속도 향상으로 성능 높이기 위함
  • 후단부(Back-end): *RTS*아키텍처 최적화 수행 - 더 효율적인 명령어로 대체해서 성능 높이기 위함

*SSA : 디스크, 클러스터 및 서버 간의 고속 데이터 전송을 용이하게 하는 데 사용되는 개방형 프로토콜

*RTS :  레지스터와 로직회로(AND, NOT 등등)를 이용하여 디지털 회로를 설계하는 레벨

*아키텍처 최적화 : 아키텍처 특성에 따라 최적화를 수행하는 것


3. 어셈블(Assemble)

 

컴파일이 끝나면 어셈블리 코드가 되고, 이 코드는 어셈블러에 의해 기계어가 된다.

 

이 단계에서는 C코드를 컴파일하고 그것을 모은다.

 

결론적으로 어셈블러를 통해 어셈블리어 파일(.s)를 오브젝트 파일(.o)로 바꿔준다.

 

이런 오브젝트 파일은 사람이 알아볼 수 없는 기계어다.

그러나 이런 오브젝트 파일은 독립적으로 실행할 수 없는데 이유는 함수를 구현한 내용이 포함되어 있지 않기 때문이다.

 

이 오브젝트 파일을 실행하기 위해서는 함수를 사용하는 오브젝트 파일 함수를 구현한 오브젝트 파일(libc.a 라이브러리)을 연결시키는 작업이 필요하다.

 

그리고 이런 연결 과정을 링킹(Linking)이라 부른다.


4. 링크(Link)

오브젝트 파일들과 프로그램에서 사용된 표준 C 라이브러리, 사용자 라이브러리를 링크한다.

해당 링킹 과정을 거치면 실행파일이 드디어 만들어진다.

링크 과정은 링커를 통해 오브젝트 파일(.o)를 묶어 실행파일로 만드는 과정이다.

 

링커는 쉽게 심볼 해석과 재배치로 나뉜다.

 

  • 심볼해석 :  여러 개의 오브젝트 파일에 같은 이름의 함수 또는 변수가 정의되어 있을 때 어떤 파일의 어떤 함수를 사용할지 결정한다.
  • 재배치 : 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 참조 주소를 알맞게 배치하는 과정

재배치

링킹을 하기 전 오브젝트 파일을 재배치 가능한 오브젝트 파일(Relocatable Object File)이라 부르고 링킹을 통해 만들어지는 오브젝트 파일을 실행 가능한 오브젝트 파일(Executable Object File)이라 부른다.

 

이 과정이 끝남으로써 helloworld.exe 가 만들어진다.



어셈블리어

어셈블리어 또는 어셈블러 언어는 기계어와 일대일 매칭이 되는 컴퓨터 프로그래밍의 저급 언어이다.

 

좀 더 자세히 알아보면 cpu 에는 해당 프로세서에 명령을 내리기 위한, 고유의 명령어가 마련되어 있는데, 이 명령어들을 기계어 라고 한다.

기계어는 각 기계마다 규약 된 숫자들의 규칙 조합으로, 기계어로 프로그래밍을 하기엔 매우 난해하다. 

가독성이 떨어지는 숫자를 대체하고자 기계어와 일대일 대응관계를 형성한 언어가 바로 어셈블리어다.

 

어셈블리어와 C언어를 간단하게 비교해 보자면

이렇게 되는데 C언어를 보면 간단하게 쓰는 것과 달리 어셈블리는 각각의 동작을 세분화시켜 저장해줘야 한다.

 

어셈블리어 중 하나인 레지스터를 알아보자.

 

레지스터는 데이터를 저장할 수 있는 작은 영역이며 프로그램이 실행되는 동안 어떤 정보를 저장하기 위해 사용된다.

이런 레지스터도 네 종류로 나뉜다.

16비트 레지스터

위에는 일반적인 목적을 가진 16비트 레지스터로 AX, BX, CX, DX로 나뉘어져 있다.

구성은 8비트로 되어 있으며 AH, AL, BH, BL과 같은 것들을 말한다.

32비트에서는 EAX, EBX, ECX, EDX로 있으며 E는 Extended(확장됨)를 의미한다. 

그렇다고 EAH, EAL과 같은 것들은 없다.

 

마지막으로 어셈블리어 명령어를 간단히 보자.