- x86 Assembly: Essential Part(2) [★]2025년 03월 23일 01시 30분 17초에 업로드 된 글입니다.이 글은 2025년 03월 23일 15시 21분 38초에 마지막으로 수정되었습니다.작성자: SONOTREE
00. 서론
지난 코스에서는 산술연산, 논리연산, 비교(cmp, test), 분기(jmp, je, jg)의 어셈블리 명령어를 배웠다. 이번 코스에서는 OS의 핵심 자료구조인 스택, C언어의 함수에 대응되는 프로시저와 관련된 어셈블리 명령어를 공부할 것이다.
- 스택: push, pop
- 프로시저: call, leave, ret
01. x86-64 어셈블리 명령어 pt.2
01.1. 스택 구조
- 스택은 하향 성장한다. 즉, 메모리를 사용할 수록 밑으로 자란다
- 스택은 top이 아래쪽에 있고, bottom이 위쪽에 있다.
- rsp라는 특수한 레지스터가 스택의 맨 아래 주소(top 원소)를 가리키고 있음
01.2. Opcode: 스택 push ★★
push val = 현재 스택의 최상단(top)에 val의 값을 저장하는 연산
스택은 밑으로 자라기 때문에(하향 성장, 주소가 감소하는 방향으로 확장) 먼저 rsp를 8 감소시켜서 새로운 공간을 만든다. 참고로 x86-64에서는 스택이 8바이트 단위로 저장된다. 그리고 이제 새로 확보한 공간에 val이라는 값을 저장한다. 즉, rsp가 가리키는 주소(rsp는 스택의 맨 아래 주소를 가리킨다)에 val을 저장하는 것이다.
rsp-=8 [rsp]=val <예제>
현재 스택 포인터(rsp)의 값은 0x7fffffffc400이다. 그리고 스택 상태는 아래의 [Stack]과 같다. 이 상태에서 push 0x31337이라는 명령어를 실행하면 어떻게 될까?
[Register] rsp = 0x7fffffffc400 [Stack] 주소 |값 0x7fffffffc400 | 0x0 <- Code 실행하기 전 rsp 0x7fffffffc408 | 0x0 [Code] push 0x31337 스택은 하향 성장하기 때문에 push, 즉 rsp를 8 감소시키면 된다. 따라서 rsp는 0x7fffffffc400 − 8 = 0x7fffffffc3f8이 되며, 새로운 rsp가 가리키는 메모리 주소에 0x31337이라는 값이 저장된다. 16진수 계산은 이런 계산기 앱을 사용하면 된다.
따라서 새로운 rsp는 0x7fffffffc3f8이 되며, 해당 주소에 0x31337이라는 값이 저장된다.
[Register] rsp = 0x7fffffffc3f8 [Stack] 0x7fffffffc3f8 | 0x31337 <- rsp 0x7fffffffc400 | 0x0 0x7fffffffc408 | 0x0 01.3. Opcode: 스택 pop ★★
pop reg: 스택 최상단의 값을 꺼내서 reg에 대입
rsp += 8 reg = [rsp-8] <예제>
현재 스택 포인터 rsp의 값은 0x7fffffffc3f이다. 그리고 rsp의 주소에는 0x31337이라는 값이 저장되어있다. 이상태에서 pop rax를 실행하면 어떻게 될까? 그리고 pop rax는 뭘 의미하는걸까?
[Rsgister] rax = 0 rsp = 0x7fffffffc3f8 [Stack] 주소 |값 0x7fffffffc3f8 | 0x31337 <-Code 실행하기 전 rsp(스택 최상단) 0x7fffffffc400 | 0x0 0x7fffffffc408 | 0x0 [Code] pop rax pop rax는 "스택에서 값을 꺼내(pop) rax에 저장"하라는 의미이다. 먼저 pop은 스택의 최상단에 있는 값을 꺼내 특정 레지스터에 저장하라는 의미이니, 현재 스택 최상단인 rsp에 있는 값을 rax에 저장한다. 이후 rsp+=8을 통해 rsp를 증가시킨다. 따라서 0x7fffffffc3f8 + 8 = 0x7fffffffc400이 된다.
[Register] rax = 0x31337 rsp = 0x7fffffffc400 [Stack] 0x7fffffffc400 | 0x0 <- rsp 0x7fffffffc408 | 0x0
02. Opcode: 프로시저
컴퓨터 과학에서 프로시저(Procedure)는 특정 기능을 수행하는 코드 조각을 의미한다. 프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체할 수 있어서 전체 코드의 크기를 줄일 수 있으며, 기능별로 코드 조각에 이름을 붙일 수 있게 되어 코드의 가독성을 크게 높일 수 있다.
프로시저를 부르는 행위를 호출(Call)이라고 부르며, 프로시저에 돌아오는 것을 반환(Return)이라고 한다. 프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(Return Address, 반환 주소)를 스택에 저장하고 프로시저로 rip을 이동시킨다.
x64 어셈블리언어에는 프로시저의 호출과 반환을 위한 call, leave, ret 명령어가 있다.
02.1. call, leave, ret
- call addr = addr에 위치한 프로시저 호출: push return_address / jmp addr
- leave = 스택프레임 정리: mov rsp, rbp / pop rbp
- ret = return address로 반환: pop rip
02.2. 스택프레임이란?
스택은 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역이다. 만약 이 스택 영역을 아무런 구분 없이 사용하게 된다면, 서로 다른 두 함수가 같은 메모리 영역을 사용할 수 있게 된다.
예를 들어, A라는 함수가 B라는 함수를 호출하는데, 이 둘이 같은 스택 영역을 사용한다면 B에서 A의 지역변수를 모두 오염시킬 수 있다. 이 경우, B에서 반환한 뒤 A는 정상적인 연산을 수행할 수 없다.
따라서 함수별로 서로가 사용하는 스택의 영역을 구분하기 위해 스택프레임이 사용된다. 대부분의 Application binary interface(ABI)에서는 함수는 호출될 때 자신의 스택프레임을 만들고, 반환할 때 이를 정리한다.
'Dreamhack > Reverse Engineering' 카테고리의 다른 글
Quiz: x86 Assembly 2 (0) 2025.03.23 Quiz: x86 Assembly 1 (0) 2025.03.23 x86 Assembly: Essential Part(1) (0) 2025.03.22 Background: Windows Memory Layout (0) 2025.03.18 Quiz: Computer Architecture [★] (0) 2025.03.18 다음글이 없습니다.이전글이 없습니다.댓글