[2주차] 문서화 과제

드림핵에서 수강한 내용에 추가해서 문서화했다.

  1. 메모리 구조
    메인 메모리(RAM)과 보조 저장장치 등등이 있다.
    보조 저장장치에 있던 프로그램 -> 실행 -> 메인 메모리에 위치한 프로세스가 됨

Memory Structure

실제 메모리는 여기저기 정렬되어 있지 않지만 virtual address로 프로세스에서는 규칙을 가짐

Memory Layout
메모리 레이아웃: 가상 메모리의 구성

섹션

데이터가 모여있는 영역
윈도우의 PE 파일: PE 헤더 + 1개 이상의 섹션

PE 헤더에 담긴 섹션 데이터: 이름, 크기, 오프셋, 속성 및 권한
이 헤더 정보를 참조하여 가상 메모리의 적절한 세그먼트에 매핑

1. .text
실행 가능한 기계 코드
읽기, 실행 권한 부여, 쓰기 권한은 거의 대부분 제거

int main() { return 31337; }

2. .data
컴파일 시점 값이 정해진 전역 변수 위치
읽기, 쓰기 권한

int data_num = 31337;
char data_rwstr[] = "writable_data";    //data

3. .rdata
컴파일 시점 값이 정해진 전역 상수와 참조할 DLL 및 외부 함수들 정보
읽기 권한

const char data_rostr[] = "readonly_data";
char *str_ptr = "readonly"; //str_ptr은 .data, 문자열("readonly")은 .rdata

섹션 외 메모리

1. 스택
각 스레드 본인만의 스택 존재
지역 변수, 함수의 리턴 주소 저장 (ex. 매개변수, 지역변수, return address…)
읽기, 쓰기 권한
아래로 확장: 낮은 주소로 업데이트

2. 힙
모든 종류 데이터 저장 가능
비교적 스택보다 큰 데이터 저장 가능
전역적 접근 가능, 동적 할당

int main() {
    int *heap_data_ptr = malloc(sizeof(*heap_data_ptr));   //동적 할당한 힙 영역 주소를 가리킴 
    *heap_data_ptr = 31337;    //힙 영역에 값 작성 
    return 0;
}

heap_data_ptr은 스택, malloc으로 할당받은 힙 영역을 가리킴

  1. 스택 프레임
    특정 함수가 가지는 공간 구조를 말한다.
    함수가 접근할 수 있는 데이터 범위 지정
    ESP, EBP 같은 포인터 레지스터와 함께 한다.

함수 호출 규약: 함수의 호출 및 반환에 대한 약속
한 함수에서 다른 함수 호출 -> 실행 흐름 옮기기 -> 호출한 함수 반환 -> 실행 흐름 옮기기
이 과정에서 Caller의 stack frame과 return address 저장 필요 <- 이래야 반환 후 실행 흐름을 옮길 수 있음
호출 시 Callee가 요구하는 인자 전달 필요, 반환 시 Caller에게 반환값 전달 필요

해당 호출 규약은 x86 아키텍처에서 쓰이는 cdecl
스택을 통해 인자 전달
인자 전달을 위해 caller가 스택 정리
스택이 아래로 자라므로 마지막 인자부터 거꾸로 push

cdecl 함수 호출 규약 실습

// Name: cdecl.c
// Compile: gcc -fno-asynchronous-unwind-tables -nostdlib -masm=intel \
//          -fomit-frame-pointer -S cdecl.c -w -m32 -fno-pic -O0

void __attribute__((cdecl)) callee(int a1, int a2) { // cdecl로 호출
}

void caller() {
   callee(1, 2);
}
; Name: cdecl.s
.file "cdecl.c"
.intel_syntax noprefix
.text
.globl callee
.type callee, @function
callee:
nop
ret ; 스택을 정리하지 않고 리턴합니다.
.size callee, .-callee
.globl caller
.type caller, @function
caller:
push 2 ; 2를 스택에 저장하여 callee의 인자로 전달합니다.
push 1 ; 1을 스택에 저장하여 callee의 인자로 전달합니다.
call callee
add esp, 8 ; 스택을 정리합니다. (push를 2번하였기 때문에 8byte만큼 esp가 증가되어 있습니다.)
nop
ret
.size caller, .-caller
.ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0"
.section .note.GNU-stack,"",@progbits

위 어셈블리를 보면, push 2, push 1로 callee의 인자값 오른쪽부터 push해주고 있다.

스택 상태가 위와 같아진다. 이제 call callee를 해주면 스택에 ret가 push된다.

ebp의 값을 push해주고, mov ebp,esp로 ebp를 끌어올려 함수의 데이터 범위를 재설정한다.
아까 코드에 없던 이유는 아마 해당 작업들이 callee에서 실행되기 때문이라고 생각한다.

지역변수를 넣어준다(없다면 생략)
변수를 넣어주면 esp는 올라가면서 esp, ebp의 범위가 다시 벌어질 텐데 해당 범위가 callee 함수가 쓸 수 있는 데이터 영역이다.
이제 함수를 종료해볼 차례이다. mov esp, ebp로 이번엔 esp에 ebp값을 옮겨서 esp를 내려준다.
*아까 위 코드 블럭에서는 8바이트만큼 할당해서 add esp, 8로 mov를 대체했다.
이후 pop ebp를 해준다. 이렇게 하면 main의 ebp값이 pop되면서 ebp가 main의 스택 프레임 범위로 복귀한다.

위에서 이제 RETN을 실행하면 esp가 가리키는 ret로 흐름이 바뀌면서 caller로 돌아오게 된다.

callee의 반환값은 레지스터(eax)에 저장된다.

함수가 시작할 때 보통
push ebp
mov ebp, esp
가 실행된다.