프로그램이 실행되면 보조기억장치(HDD, SSD)에 저장되어 있던 프로그램이 주기억장치(RAM)에 로드된다. 또한 해당 프로그램이 실행되기 위한 데이터들을 저장하기 위해 OS는 해당 프로그램에 메모리 영역을 할당한다. 이렇게 할당된 메모리 영역은 크게 4가지 영역으로 구분된다.
추후에 나오게 될 BOF, 어셈블리어의 동작 방식과 관련이 많다.
레지스터는 아키텍쳐마다 상이하다. 이 글에서는 x86-64 아키텍쳐의 레지스터에 대해 다룬다.
64bit 레지스터는 OS의 최적화에 의해 32bit(E_X), 16bit(_X) 등으로 나누어져 사용될 때도 있다.
어셈블리어를 분석할 때 레지스터에 왜 이런 값이 저장되는지 모르겠을 경우 구글링을 해보자. 호출 규약(calling convetion)과 관련이 있을 가능성이 높다.
주로 Index addressing(인덱스 주소 지정)시 사용되며 덧셈과 뺄셈 연산에도 사용된다.
윈도우에서의 레지스터 인자 전달 순서 :
rcx - rdx - r8 - r9 - rest on stack(스택을 이용해 인자를 전달한다는 뜻이다.)
이외의 운영 체제에서의 레지스터 인자 전달 순서 :
rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
이러한 인자 호출 순서를 calling convetion이라 한다.
이 부분에서 등장하는 Stack은 앞서 언급되었던 메모리 구조의 Stack을 의미한다.
메모리의 주소값을 저장한다.
ESP(Stack Pointer) : Stack frame의 끝 지점 주소(가장 낮은 메모리 주소)를 저장한다.
EBP(Base Pointer) : Stack의 시작 지점 주소(가장 높은 메모리 주소)를 저장한다.
EIP(Instruction Pointer) : 다음에 실행해야 할 명령어의 주소를 저장한다.
각 영역의 시작 주소를 저장해 이를 기준삼아 메모리 상의 위치를 구할 수 있게 해준다.
CS(Code Segment) : 메모리 구조의 코드 영역(code segment)의 시작 주소를 저장한다.
DS(Data Segment) : 메모리 구조의 데이터 영역(data segment)의 시작 주소를 저장한다.
SS(Stack Segment) : 메모리 구조의 스택 영역(stack segment)의 시작 주소를 저장한다.
ES(Extra Segment), FS, GS : 부수적으로 데이터를 저장하기 위한 Segment이다.
다양한 연산들의 결과 상태를 저장한다. 조건문의 실행 분기를 결정할 때 주로 사용된다.
아키텍쳐에 따라 어셈블리어가 다르다. 이 글에서는 x86-64 아키텍쳐의 어셈블리어를 기준으로 설명한다.
어떤 문법이 어떤 구조를 가지는지 외울 필요는 없고 그냥 이런게 있다는 걸 알고 넘어가면 된다. 필요할 때 찾아보도록 하자.
접두사
AT&T : 레지스터는 '%' 접두사를 가지며 값들에는 '$' 접두사를 가진다.
Intel : 문법이 대체로 간단하고 접두사가 없다. 16진수와 2진수 데이터는 'h', 'b'의 접두사를 가진다.
왜 h가 뒤에 붙는데 접두사인지는 필자도 잘 모르겠다.
피연산자(Operend)의 위치
AT&T : instr src,dest
Intel : instr dest,src
메모리 피연산자
AT&T : '()'를 사용해 표현
Intel : '[]'를 사용해 표현
접미사
AT&T : 정해진 규칙에 따라 접미사를 사용한다.
Intel : 접미사를 사용하지 않는다. 문장 그 자체로 의미를 가지도록 사용한다.
그럼 Instruction과 Opcode의 차이점이 무엇일까? 링크에 따르면 같은 Instruction이라도 Operands의 종류에 따라 다른 Opcode를 가질 수 있다고 한다. MOV EAX, EBX와 MOV EAX, 8 의 Opcode가 다를 수 있다는 것. 따라서 MOV, LEA 등등은 Opcode가 아닌 Instruction으로 봐야한다.
MOV(Move) : src의 데이터를 복사하여 dest에 저장한다.
LEA(Load Effective Address) : src의 주솟값을 dest에 저장한다.
PUSH : stack frame에 src 데이터를 저장한다.
POP : dest에 stack frame 데이터를 저장한다.
ADD/SUB/MUL/DIV(Add/Subtract/Multiply/Divide) : 차례대로 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 수행한다. (ex : dest = dest - src)
INC/DEC(Increase/Decrease) : 차례대로 증가 연산, 감소 연산이다.
AND/OR/XOR/NOT : 차례대로 and, or, xor, not 논리 연산이다.
CMP(Compare) : dest와 src의 값을 비교해 같다면 ZF의 값을 0으로 만든다.
JMP/JE/JNE: PC의 값을 변경해 실행 흐름을 이동(JUMP)시킨다. JE(Jump if Equal)는 ZF의 값이 0일 경우, JNE(Jump if Not Equal)는 ZF의 값이 1일 경우 점프시킨다.
JLE(Jump if Less or Equal), JGE(Jump if Greater or Equal), JG(Jump if Greater) 등의 여러 jump 계열 명령어가 존재한다.
추가적인 명령어들은 이곳을 참고하면 좋을 것 같다.
CALL : EIP의 값을 Stack의 최상단에 넣고 함수를 호출한다.
RTN : Stack 최상단의 주소를 EIP에 저장한다. 이 주소는 복귀 주소가 된다.
NOP : 공백 처리. 쉬어가는 명령어이다.
예시들은 모두 Intel 문법으로 작성되었다.
메모리에 접근하는 가장 일반적인 형태의 주소 지정 방식은 [RB+S*RI+D] 의 형태이고 Segment는 암묵적으로 Data Segment이다.
RB(Base Register) : 말 그대로 base의 역할을 한다.
S(Scale) : RI에 곱해지는 수로 몇 개의 주소를 건너 뛸 지 정한다.
RI(Index Register) : 배열의 index와 유사한 역할을 한다.
D(Displacement) : 주소 지정에서 변위의 역할을 한다.
아래 등장하는 모든 주소 지정 방식의 이름을 외울 필요는 없다.
결국 Complete Memory Addressing Form으로 전부 표현할 수 있기 때문이다.
레지스터 주소 지정(Register Addressing)
ex)
MOV EDX, COUNT ; 첫 번재 피연산자가 레지스터이다.
MOV COUNT, ECX ; 두 번째 피연산자가 레지스터이다.
MOV EAX, EBX ; 두 피연산자가 모두 레지스터이다.
직접 메모리 주소 지정(Direct Memory Addressing)
ex)
BYTE_VALUE DB 150 ; BYTE_VALUE가 정의된다. DB는 데이터의 크기를 나타낸다.
ADD BYTE_VALUE, DL ; BYTE_VALUE의 메모리 위치에 DL의 값을 더한다.
MOV AL, DS:[8088h] ; AL에 Data Segment의 offset 위치의 값을 저장한다.
즉시 주소 지정(Immediate Addressing)
ex)
BYTE_VALUE DB 150 ; BYTE_VALUE가 정의된다.
ADD BYTE_VALUE, 35 ; BYTE_VALUE의 값에 35를 더한다.
레지스터 간접 주소 지정(Register Indirect Addressing)
ex)
MOV AL, [BX] ; AL에 DS:[BX]의 값을 저장한다.
인덱스 주소 지정(Indexed Addressing)
ex)
MOV AL, [BX+20h] ; AL에 DS:[BX+20h]의 값을 저장한다.
인덱스 주소 지정 방식은 어셈블리 코드를 분석할 때 등장하게 되는 RIP Relative Addressing과 관련 있다.
베이스 레지스터 주소 지정(Base Register Addressing)
ex)
MOV AL, [BX+SI] ; AL에 DS:[BX+SI]의 값을 저장한다.
변위(displacement)를 갖는 인덱스 주소 지정
ex)
MOV AL, [BX+SI+30h] ; AL에 DS:[BX+SI+30h]의 값을 저장한다.
라이브러리 콜과 구분되는 개념이며 해당 내용은 시스템 프로그래밍에서 배울 수 있다.
Stack Frame이란 메모리 구조의 Stack에 차례대로 저장되고 소멸되는 각 함수의 호출 정보(매개 변수, 반환 주솟값, 변수 etc)를 저장하고 있는 부분이다.
#function prolouge explain
push rbp # push, pop 연산은 모두 rsp를 기준으로 일어난다.
mov rbp, rsp
sub rsp, 0x30 # 임의의 상수(예시에서는 48)만큼 스택 프레임을 할당한다.
#function epilouge explain
mov rsp, rbp
pop rbp
pop rip