Ⅰ. 개요
CPU(Central Processing Unit) Architecture의 구조를 살펴본다. 설계방식에 따른 CISC(Complex Instruction Set Computer), RISC(Reduced Instruction Set Computer)의 차이를 알아본다. CPU Architecture의 Register 개념과 종류에 대해 소개한다. Memory Architecture의 구조를 살펴본다. MMU(Memory Management Unit)의 역할과 가상 메모리(Virtual Address)의 연결 구조를 알아본다. 개발된 프로그램이 어떻게 실행되는지 과정을 살펴보고 Debugger의 개념과 실제 실습한 것 중 알게된 부분을 설명한다. 어셈블리 언어의 명령어와 OpCode, Operand를 이야기한다.
Ⅱ. CPU Architecture
CPU(Central Proccessing Unit)
CPU(Central Processing Unit)는 중앙 처리 장치로 컴퓨터에서 기억, 해석, 연산, 제어라는 큰 역할을 한다. 지금 공부하는 내용은 CPU를 어떻게 만드는지가 아닌 동작 구조를 이해하고 프로그램 구조를 해석할 때 어떻게 파악하는지에 초점이 맞춰져있다. 그러므로 그에 관련한 내용만 짚고 가자면, 우선 Stored-program concept을 이해해야 한다. 폰 노이만의 고안한 프로그램 내장 방식으로 현대 컴퓨터는 모두 이 방식으로 동작한다고 봐도 된다. 이 방식은 방금 이야기한 것처럼 프로그램(애플리케이션) 내장 방식으로 애플리케이션을 기억 장치(메모리)에 저장해놓고 CPU가 한 줄(하나의 데이터)씩 명령을 수행한다. 수행의 결과를 다시 메모리에 기록하고 다시 다음 줄의 명령을 가져와서 수행한다. 이 과정을 무한 반복하는 것이 Stored-program concept이다.
CISC(Complex Instruction Set Computer), RISC(Reduced Instruction Set Computer)

그렇다면 CPU는 Stored-program concept을 어떻게 수행할 것인가?크게 CISC와 RISC 설계방식이 있다. CISC는 위 표에서 보는 것처럼 복잡 명령어, 가변 길이 명령어를 수행할 수 있고 호환성이 좋지만 속도가 느리다. 상대적으로 비싸고 고전력을 차지한다. 일반 데스크탑에서 많이 볼 수 있는 Intel, AMD가 있다. RISC는 단순 명령어, 고정 길이 명령어를 수행하고 대신 속도가 빠르다. 상대적으로 저렴하고 저전력을 차지한다. 그래서 우리가 흔히 사용하는 스마트폰의 메인 칩이라고 불리는 것들이 ARM, Mips, GPU가 있다.
※ 프로세서와 프로세스 차이를 알아두면 좋은데, 프로세서는 CPU, GPU와 같이 물리적 장치를 의미하고 프로세스는 실행 중인 프로그램(애플리케이션)을 이야기 한다.
※ CISC 설계 방식 CPU가 있어야 윈도우와 같은 운영체제를 설치할 수 있는데, 맥북의 경우 RISC 설계 방식의 CPU를 사용하기 때문에, 일반적인 방법으로 윈도우를 사용할 수 없다.
Register
위에서 CPU는 Stored-program concept을 수행한다고 했다. 그래서 CISC와 RISC 설계 방식에 따라 CPU는 작동할텐데 그렇다면, 실제로 어떻게 프로그램을 저장하고 명령(데이터)를 수행하고 그 결과를 다시 저장할 수 있는 것인가?
그건 바로 CPU내의 Register 때문이다. Register는 다목적 저장 공간으로 General Purpose Register 32bit 8개와 Segment Register 16bit 6개, Program Status and Control Register 32bit 1개, Instruction Pointer 32bit 1개로 구성되어 있다.
① General Purpose Register
범용 레지스터로서 다양한 값을 저장한다.
범용 레지스터 | 저장하는 값 종류 |
EAX | 산술 연산된 값, 함수의 리턴(Return) 값 |
EBX | ESI, EDI 레지스터와 결합할 수 있고 메모리 주소를 저장 |
ECX | 반복문에서 반복 횟수(카운터)를 저장하고 수행 |
EDX | EAX와 같이 쓰이며, 산술 연산의 보조 역할(부호 확장, 큰 수의 곱셈, 나눗셈 등) |
ESI, EDI | 데이터를 조작(문자열 비교)하거나 복사 시 ESI는 소스 데이터 주소(원본 데이터 주소), EDI는 목적지 주소(복사 후 이동될 목적지 주소)가 저장 |
ESP | 스택(Stack)의 가장 아랫부분, 마지막 주소가 저장 |
EBP | 스택(Stack)의 가장 윗 부분, 처음 주소가 저장 |
※ 이후에 다루겠지만, 스택(Stack)은 LIFO(Last In First Out) 구조이다. 데이터를 넣을 때 제일 마지막에 들어와서 제일 먼저 나간다.
② Segment Register
현재 수행되고 있는 명령어의 위치를 나타낸다. 프로그램이 사용하는 데이터가 있는 메모리 주소를 나타낸다.
세그먼트 레지스터 | 저장하는 값 종류 |
CS | Code Segment, 코드 영역의 시작 주소 |
SS | Stack Segment, 스택 영역의 시작 주소 |
DS | Data Segment, 데이터 영역의 시작 주소 |
ES, FS, GS | Data Segment, 데이터 영역의 시작 주소이지만, ES의 경우 문자열, FS의 경우 추가적인 데이터(특별한 목적 데이터), GS의 경우 다양한 응용프로그램에 대해 저장 |
③ Program Status and Control Register
컨트롤 레지스터 | 저장하는 값 종류 |
Carry Flag(CF) | 덧셈과 뺄셈에 의한 빌림수(Borrow) 발생 시 1로 설정 |
Parity Flag(PF) | 연산 결과가 짝수면 1, 홀수면 0으로 설정 |
Auxiliary Flag(AF) | 16(8)비트 연산 시 빌림수 발생 시 1로 설정 |
Zero Flag(ZF) | 연산 결과가 0(같을 때)이면 1로 설정 |
Sign Flag(SF) | 연산 결과가 1(음수, 부호 비트)이면 1로 설정 |
Overflow Flag(OF) | 연산 결과가 용량 초과일 경우 1로 설정 |
※ Zero Flag(ZF)의 경우 일반 산술 비교 연산만이 아닌 문자열 비교 연산 수행 후 결과가 같으면 0으로 1로 설정된다. 만약 0이라면 결과가 다르다고 볼 수 있다. 이 레지스터를 조작하여 코드의 조건을 변경할 수 있다.
④ Instruction Pointer
IP Register로서 네트워크의 IP 주소가 서버의 식별자로서 위치를 알려주듯, 프로그램이 실행할 코드(실행할 명령)의 주소를 알려준다. 32bit인 경우 EIP, 64bit의 경우 RIP이다.
※ IP Register의 변경을 통해 프로그램의 동작을 마음대로 제어할 수 있다. RCE(Reverse Engineering): 리버싱이라고 불리며, 기존 프로그램의 코드를 분석하는 원래 소스 코드로 복원하는 걸 뜻한다. 해킹(악성코드)에 사용된다.
※ 취약점 진단: 코드의 문제점(허점)을 공격자가 마음대로 코드를 변경시켜 버그를 발생시킨다. 윈도우에서 블루스크린을 뜨게 하거나, 일반 프로그램(애플리케이션)의 경우 오류창이 뜬다. 이를 어떻게 대비할 수 있는지, 취약한 부분이 어디인지 찾는 게 취약점 진단이다.
※ CPU 연산 과정에서 Big-endian과 Little-endian이 있다. Big-endian은 보통 우리가 숫자를 쓰는 것처럼 높은 숫자부터 낮은 숫자로 연산되는 과정이고 Little-endian은 반대로, 낮은 숫자부터 높은 숫자로 연산되는 과정이다. Big-endian은 인간의 눈으로 가독이 쉽지만, CPU는 변환 과정을 거쳐야해서 Little-endian에 비해 느리다. 보통 CPU는 Little-endian으로 처리된다고 보면 된다.
Ⅲ. Memory Architecture
Memory
CPU가 작업을 수행하는 공간으로 보통 RAM이라고 부르는 것이라고 생각하면 된다. 속도가 디스크에 비해 빠르고, CPU와 마찬가지로 폰 노이만 아키텍쳐이다. 대신 한정된 리소스를 가지기 때문에, 휘발성이다. 예전엔 Single Tasking으로 비효율적이었는데, 하나의 프로그램이 마무리될 때 까지 다른 프로그램이 작업할 수 없어 프로세서의 자원을 낭비했다. 그러나 하나의 CPU에서도 Multi Tasking(멀티 태스킹)을 할 수 있는 스케쥴링 기법을 통해 요즘의 다중 작업이 가능해졌다.
※ 윈도우에서 동작 프로세스 목록을 보는 명령어로 'tasklist'가 있다. 이때 나오는 PID는 실행할 때 마다 랜덤이다.
스케쥴링을 통해 병렬 연산이 가능해졌다고 하는데, CPU와 RAM은 한정적인데 어떻게 가능한 것인가?

바로 가상 메모리 때문이다. 옆에 사진에서 보이는 것처럼, 우선 실제 메모리가 아닌 가상의 메모리를 만든다. 가상 메모리에는 사진처럼 각각 A, B, C 프로그램(애플리케이션)이 메모리를 하나씩 차지하고 있다고 착각하게 만든다. 그렇다면 물리 메모리는 어떻게 사용하는 것일까?
아래의 이미지처럼 MMU(Memory Management Unit)의 존재 때문이다. MMU는 가상 메모리를 실제 물리 메모리에 매핑해주는 역할을 한다. CPU가 프로세스를 동작시키면서 메모리에 Access(접근)할 때마다 가상 메모리(Virtual Memory Address)에 있는 주소를 물리 메모리 주소(Physical Memory Address)로 매핑해준다.
※ 나중에 할 것이라고 생각되지만, 실제로 메모리는 RAM만이 아닌 디스크도 같이 사용한다. 윈도우의 경우 설정 - 시스템 - 정보(맨 아래) - 고급 시스템 설정(중간) - 고급 탭의 설정 - 고급(가상 메모리)가 보일 것이다.

그렇다면 이제 실제 동작 방식을 살펴보자. 일반적으로 얘기하는 DRAM은 Stack, Heap, 코드 영역, 데이터 영역의 여러 값의 저장소인데, DRAM말고 SRAM이 있다. SRAM(Static Random Access Memory)은 고정 메모리로서 DRAM(Dynamic Random Access Memory)에 저장된 여러 프로그램(위 사진에서 A, B, C) 중 A 프로그램이 먼저 동작한다고 했을 때 우선적으로 CPU의 레지스터를 할당해줬다가 B 프로그램이 동작할 때 기존의 레지스터에 바꿔서 할당해준다. 즉, 스위칭(Conetxt Switching)을 한다. SRAM은 속도가 빠르고 고가의 장비라 SRAM이 많은 CPU는 그만큼 다중 작업에 탁월한 성능을 보여준다. 결론적으로 DRAM의 물리 메모리에 실행하는 프로그램들이 모두 저장(여러 가상 메모리에 저장시킨다.)되어 있다가 작업할 때 마다 SRAM이 스위칭을 통해 동시(멀티 태스킹)에 작업시킬 수 있게 되는 것이다.
추가적으로 이 멀티 태스킹 작업을 위해 초반에 언급한 스케쥴링 기법을 사용하는데, 관련 기법으로 하나의 프로세서(CPU, GPU)에서 각각의 프로그램(프로세스, 애플리케이션)을 교대로 수행하는 TDM(Time Division Multiplex)이 있다.

TDM은 몇 초의 시간마다 어떤 가상 메모리를 동작시킬지 랜덤으로 결정한다. 가상 메모리가 동작하는 게 순서대로 바뀌며, 실제로 실행되고 있는 프로그램에게는 물리적인 메모리를 감춘다(실제로 물리 메모리에 할당되어 있지만). 그리고 페이지(Page)라는 가상 메모리 주소 공간은 일정한 크기로 독립되어 있다.
만약, C언어로 응용 프로그램을 개발한다고 했을 때, 컴파일 => 링커(목적코드 .obj) => 파일 시스템(실행 파일, EXE 등) => 로더(커널과 메모리에 응용 프로그램 실행 파일 할당) 순으로 동작한다.


위 사진을 보면 이해가 용이할 수 있다. C언어는 메모리를 직접 관리하는 언어이기 때문에, 메모리 공간에 할당하고 주소값이 어떻게 매핑되는지 이해하기 용이하다고 한다. 많은 응용 프로그램이 C언어로 개발되었으며, 윈도우 용 실행 파일(EXE)이 컴파일과 링커(Binary - 기계어로 컴퓨터가 실행시킬 수 있는 파일)과정을 통해 우리가 이용하는 여러 응용 프로그램이 되는 것이다. 실행 파일의 헤더 부분은 아래에서 PEview를 얘기하며 하겠지만, 프로그램 실행과 관련된 정보(모듈, IAT, EAT, 메모리 등)의 정보가 포함되어 있다.
※ C언어만은 아니지만, 컴파일 과정에서 Stub이라는 컴파일러가 추가하는 코드가 있다. 데이터베이스 연동, 네트워크 연결, 테스트, 환경 변수 설정 등 실행 흐름을 제어하거나 아직 구현되지 않은 함수이거나 하드웨어를 제어해야 할 때 컴파일러가 자동으로 추가한다.
Ⅳ. Debugger
디버그는 이미 친숙할 수 있는데, 컴퓨터의 작동 오류를 버그라고 하고 그 버그를 찾거나 수정하는 작업이다. 윈도우의 실행 파일의 종류로 EXE, SCR, DLL, SYS 등이 있다. 모두 PE(Portable Executable) Format(포맷) 혹은 Structure(구조)를 따른다. 간단하게, PE 구조는 윈도우에서 실행 파일이 어떤 구성으로 되어 있는지를 정의한 것으로 일반적으로 EXE를 많이 볼 수 있을 것이다. 그 외에 생소해보이는 SCR은 화면보호기 실행 파일이고 DLL(Dynamic Link Library)은 대표적으로 커널(KERNEL32.dll)이 있다. SYS는 드라이버 관련 실행 파일이라고 보면 된다. 또, 윈도우에서 실행 가능한 스크립트용 파일로 BAT, PS1이 있다. 이 스크립트 파일은 명령어 자동화 코딩을 통해 많이 쓴다.
※ 취약점 진단의 경우 운영체제에 종속된 스크립트로 많이 한다. 예를 들어, 고객의 서버에 별도의 프로그램을 설치하는 걸 상상해보자. 가능하겠는가? 당연히 고객 컴퓨터를 함부로 건드릴 수 없기 때문에, 안된다고 보면 된다. 꼭 그런 건 아니겠지만, 일반적으로 그렇다고 한다.
※ KERNEL32.dll은 윈도우 애플리케이션 인터페이스의 모든 함수가 정의되어 있어 여러 응용프로그램들은 이 커널을 동적으로 참조하여 사용한다. 자세한 내용은 Microsoft 사이트에서 볼 수 있다.
Windows Kernel-Mode 커널 라이브러리 - Windows drivers
※ SYS 드라이버 실행 관련 파일이 있는 이유는 윈도우는 드라이버에 직접 접근이 안되고 드라이버라는 API를 통해 접근해야 하기 때문이다.
DLL파일에 대해 조금 더 설명해보자면, 윈도우 실행 파일에는 IAT(Import Address Table)과 EAT(Export Address Table)이 있다. 코드의 유지보수 관리를 위해 DLL은 여러 함수, 클래스를 포함한 모듈로 작게 만들어졌는데, IAT와 EAT는 바로 DLL파일을 참조해서 모듈의 함수를 실행시킨다. 실행 파일 안에 있는 IAT로 어떤 윈도우 DLL파일의 함수들을 써야할지 정리한 다음 EAT가 참조해서 외부에 있는 DLL파일의 함수들을 호출하고 사용 후 해제한다.
Debugger의 Tool로 여러 가지가 있는데, 이번에 알게 된 Tool은 GHIDRA(기드라), Cheat Engine, HxD, OllyDbg, PEview, IDA 등이 있다. Debugger Tool은 해킹 도구로도 사용될 수 있는데, HxD의 경우 16진수로 PE 파일들을 보여주고 OllyDbg는 이번에 실습으로 코드를 분석해보는 연습을 하는 데 사용할 수 있었다.

글자가 너무 작아 안보일 수 있겠지만 빨간색 부분은 어셈블리(Assembly) 명령어로 작동하는 프로그램 코드를 볼 수 있는 곳이고 오른쪽 노란 부분은 Register 부분, 아래의 오른쪽 보라색 부분은 스택(Stack) 영역, 마지막으로 왼쪽 아래 보라색 부분은 메모리 덤프 부분이다.
PEview 프로그램을 활용해서 'IMAGE_OPTIONAL_HEADER', 'SECTION .rdata'를 통해 프로그램의 실행 관련 여러 정보를 얻을 수 있다. 일단 배운 부분만 적어보자면, 두 경우 모두 PE Header에 명시되는데, PE Header는 실행과 관련된 전반적인 정보(드라이버, DLL, 메모리 주소 정보 등)이 포함되어 있다. 'IMAGE_OPTIONAL_HEADER'에서 주목해야할 부분은 'Address of Entry Point'와 'Image Base'이다. 'Address of Entry Point'는 실행 파일이 메모리에 매핑된 후 코드 시작 지점을 의미한다. 'Image Base'는 메모리에 할당될 때, 기준이 되는 주소 값을 가지고 있다. 'Image Base'는 보통 EXE의 경우 0x0040000이고 DLL의 경우 0x10000000이다. 그래서 두 요소를 더한 값이 프로그램이 시작 지점이 된다. 추가적으로 'Base of Code'가 있는데, 이는 코드 섹션(전체 코드)의 시작 지점을 의미한다. 보통 'Address of Entry Point'와 'Base of Code'는 같다. 마지막으로 'Number of Data Directories'는 데이터 디렉토리의 개수를 나타낸다. 'SECTION .rdata'에는 위에서 설명한 IAT 내용이 포함되어 있다.

이외에 프로그램은 아직 활용해보지 못해 공부가 필요하다.
이런 Debugger Tool을 이용해서 실행 파일을 분석하기 위해 Assembly 언어를 알면 좋은데, 일단 OpCode와 Operand가 있다. OpCode는 실제 동작 수행과 관련된 명령어들로


이런 명령어들이 있다. Operand(오퍼코드)는 피연산자로서 어떤 데이터를 대상으로 연산 과정들이 수행될지 나타내며, 있을 수도 있고 없을 수도 있다. 즉, Operand(파라미터 - Parameter)는 연산할 대상으로 레지스터나 메모리 주소, 참조값 등이라고 생각하면 된다.
후기
애플리케이션 이해 및 활용 첫 수업이 시작되었습니다. 온라인 수업이라 이론 수업 위주로 나왔는데, 확실히 이론 수업은 어렵습니다. 거기다가 실제로 한 학기는 쏟아야 배울 내용을 축약해서 하다보니 이 블로그 포스팅을 위해 알려주신 내용과 키워드를 바탕으로 추가적으로 더 공부해야해서 3시간은 넘게 걸린 것 같습니다...🤣 아직 많이 이해가 부족하니, 조금 더 열심히 해볼 필요가 있을 것 같습니다.
Log in