이 글은 책 컴퓨터 밑바닥의 비밀 chapter 3.2-3.3 의 내용을 읽고 요약한 글입니다.
3.2 프로세스는 메모리 안에서 어떤 모습을 하고 있을까?
3.2.1 가상 메모리: 눈에 보이는 것이 항상 실제와 같지는 않다
- 모든 프로세스의 코드 영역은 0x400000에서 시작
- 서로 다른 두개의 프로세스가 메모리를 할당하기 위해 malloc을 호출하면 동일한 시작 주소를 반환할 가능성이 매우 높음
Ubuntu 에서 테스트
// malloc_test.c
#include <stdio.h>
#include <stdlib.h>
int main() {
void *ptr = malloc(10); // Allocate 10 bytes
printf("Address returned by malloc: %p\\n", ptr);
free(ptr); // Don't forget to free the memory
return 0;
}
./malloctestAddressreturnedbymalloc:0x56342fa8d260 ./malloc_test Address returned by malloc: 0x55a3e5a9d010
다른 주소가 반환됨. 이유는? → ASLR 이라는 기술이 최근 OS에는 적용되어있음
ASLR (Address Space Layout Randomization)
- 스택, 힙, 동적 라이브러리 영역의 주소를 랜덤으로 배치해서 공격에 필요한 target address를 예측하기 어렵게 만드는 기술
ASLR 기술을 끄고 테스트하면 같은 주소가 반환되는 걸 볼 수 있다
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ ./malloc_test
$ Address returned by malloc: 0x40068c
$ ./malloc_test
$ Address returned by malloc: 0x40068c
- 두 프로세스가 모두 같은 주소에 데이터를 쓸 수 있다는 의미인데 괜찮은가?
⇒ 주소값은 가짜 주소(가상 메모리 주소)이고 메모리에 조작이 일어나기 전 실제 물리 메모리 주소로 변경되기 때문에 문제 되지 않음

주목할 2가지 사항
- 프로세스는 동일한 크기의 chunk로 나뉘어 물리 메모리에 저장
- 위 그림에서 힙 영역은 3개의 chunk로 나뉨 - 모든 조각은 물리 메모리 전체에 무작위로 흩어져 있음
보기에 아름답지 않지만 운영 체제가 프로세스에 균일한 가상의 주소 공간을 제공하는 것을 방해하지는 않음 → 가상 메모리와 물리 메모리 사이의 mapping을 나타내는 page table로 관리
3.2.2 페이지와 페이지 테이블: 가상에서 현실로

- 각각의 프로세스에는 단 하나의 페이지 테이블만 있어야 함
- mapping은 ‘page’ 라는 단위로 이루어짐
- 프로세스의 주소 공간을 동일한 크기의 ‘조각’으로 나누고, 이 ‘조각’을 페이지(page)라고 부름
- 그러므로 두 프로세스가 동일한 메모리 주소에 기록하더라도 페이지 테이블 통해 실제는 다른 물리 메모리 주소에 저장됨
3.3 스택 영역: 함수 호출은 어떻게 구현될까?
아래 코드의 문제점은?
void func(int a)
{
if(a>100000000)
{
return;
}
int arr[100] = { 0 };
func(a + 1);
}
- 함수 실행 시간 스택(runtime stack)과 함수 호출 스택(call stack) 이해 필요
3.3.2 함수 호출 활동 추적하기: 스택
- A, B, C, D 4가지 단계가 존재하고 아래그림 처럼 의존성을 가짐

단계별로 진행 과정

- A→B→D→B→A→C→A
- 선입 선출(Last In First Out, LIFO) 순서로 stack 과 같은 데이터 구초가 처리하기 적합
3.3.3 스택 프레임 및 스택 영역: 거시적 관점
- 함수 실행시의 자신만의 ‘작은 상자’가 필요한데 이를 call stack 혹은 stack frame 이라고 함
- 스택은 낮은 주소 방향으로 커지므로 아래와 같이 표현됨

- stack frame에는 어떤 정보들이 포함되는지?
3.3.4 함수 점프와 반환은 어떻게 구현될까?
- 함수 A가 함수 B를 호출하면, 제어권이 A에서 B로 옮겨짐
- 제어권: CPU가 어떤 함수에 속하는 기계 명령어를 실행하는지 의미
제어권이 넘어갈 때는 2가지 정보가 필요함
- 반환(return): 어디에서 왔는지에 대한 정보
- 함수 A의 명령어가 어디까지 실행되었는지에 대한 정보
- 점프(jump): 어디로 가는지에 대한 정보
- 함수 B의 첫 번째 기계 명령어가 위치한 주소
위의 정보는 어디서 가져오는지? → 스택 프레임!
함수 A가 함수 B를 호출할 때 CPU는 함수 A의 기계 명령어(주소: 0x400564)를 실행중이라 가정

- CPU는 다음 기계 명령어를 실행하는데 call 뒤에 명령어 주소가 함수 B의 첫번째 기계 명령어
- 이 명령어를 실행한 직후 CPU는 함수 B로 점프하게 됨
- 함수 B실행이 완료되면 어떻게 함수 A로 돌아오나?
- call 명령어 실행 후 지정한 함수로 점프 + call 명령어 다음 위치 주소(0x40056a)를 함수 A의 스택 프레임에 넣음

반환 주소가 추가되면서 스택 프레임이 아래 방향으로 조금 커짐

- 함수 B에 대응하는 기계 명령어를 실행하면서 B에 대한 스택 프레임도 추가됨
- 함수 B의 마지막 기계 명령어인 ret은 CPU에 함수 A의 스택 프레임에 저장된 반환주소로 점프하도록 전달하는 역할
3.3.5 매개변수 전달과 반환값은 어떻게 구현될까?
대부분의 경우 레지스터를 통해 매개변수와 반환값을 전달
- 함수 A가 B를 호출하면, A는 매개변수를 상응하는 레지스터에 저장
- CPU가 함수 B를 실행할 때 이 레지스터에서 매개변수 정보를 얻을 수 있음
- CPU 내부의 레지스터 수가 제한되는 경우 나머지는 스택 프레임에 넣음

3.3.6 지역 변수는 어디에 있을까?
- 레지스터에 저장할 수 있지만 로컬 변수가 레지스터 수보다 많으면 스택프레임에 저장

3.3.7 레지스터의 저장과 복원
- 함수 A와 B가 지역변수 저장을 위해 모두 레지스터를 사용하면 값이 덮어써 질 수 있음
- 그렇기 때문에 초기에 저장된 값을 레지스터에서 사용하고 나면 다시 그 초기값을 함수의 스택 프레임에 저장해야 함

3.3.8 큰 그림을 그려보자, 우리는 지금 어디에 있을까?

void func(int a)
{
if(a>100000000)
{
return;
}
int arr[100] = { 0 };
func(a + 1);
}
- 앞에서 본 코드를 다시 보면, 자기 자신 함수를 100000000번 호출
- 호출 할 때마다 스택 프레임이 함수 실행시 정보를 저장하기 위해 생성되며, 함수 호출 단계가 증가하며 스택 영역이 점점 더 많은 메모리 차지 → stack overflow 발생
Python Tutor code visualizer: Visualize code in Python, JavaScript, C, C++, and Java
Please wait ... your code is running (up to 10 seconds) Write code in Python 3.11 [newest version, latest features not tested yet] Python 3.6 [reliable stable version, select 3.11 for newest] Java C (C17 + GNU extensions) C++ (C++20 + GNU extensions) JavaS
pythontutor.com

- 코드가 실행되면서 stack frame이 계속 쌓이는 모습을 볼 수 있음
'OS' 카테고리의 다른 글
[책리뷰] 컴퓨터 밑바닥의 비밀: ch4.1 이 작은 장난감을 CPU라고 부른다 (0) | 2024.09.01 |
---|---|
[책리뷰] 컴퓨터 밑바닥의 비밀: ch3.4 메모리 힙 영역 (0) | 2024.09.01 |
[책리뷰] 컴퓨터 밑바닥의 비밀: ch3.1 메모리의 본질, 포인터와 참조 (0) | 2024.08.25 |
Event loop와 coroutine (0) | 2024.08.11 |
블로킹/논블로킹 (0) | 2024.08.10 |