Fascination
article thumbnail

Background: Linux Memory Layout


# 서론

- 컴퓨터는 크게 CPU와 메모리로 구성되어 있음

- CPU는 실행할 명령어와 명령어 처리에 필요한 데이터를 메모리에서 읽고, Instruction Set Architecture(ISA)에 따라 이를 처리함.

  그리고 연산의 결과를 다시 메모리에 적재

  > 이는 CPU의 동작과 메모리 사이에 밀접한 연관이 있음을 의미

- 만약 공격자가 메모리를 악의적으로 조작할 수 있다면 조작된 메모리 값에 의해 CPU도 잘못된 동작을 할 수 있음

- 이것을 메모리가 오염되었다고 표현하며, 이를 유발하는 취약점을 메모리 오염(Memory Corruption) 취약점이라고 부름

- 시스템 해킹에 다양한 기법들이 있지만, 많은 공격 기법이 Memory Corruption을 기반으로 하고 있음

- 개요

  • 세그먼트란
  • 코드 세그먼트
  • 데이터 세그먼트
  • BSS 세그먼트
  • 스택 세그먼트
  • 힙 세그먼트

 

 

# 리눅스 프로세스의 메모리 구조

1. 세그먼트

- 리눅스에서는 프로세스의 메모리를 크게 5가지의 세그먼트(Segment)로 구분

- 세그먼트란 적재되는 데이터의 용도별로 메모리의 구획을 나눈 것인데, 크게 코드 세그먼트, 데이터 세그먼트, BSS 세그먼트, 힙 세그먼트, 그리고 스택 세그먼트로 구분

- 운영체제가 메모리를 용도별로 나누면, 각 용도에 맞게 적절한 권한을 부여할 수 있다는 장점이 있음

- 권한은 읽기, 쓰기, 그리고 실행이 존재하며, CPU는 메모리에 대해 권한이 부여된 행위만 할 수 있음

  ex) 데이터 세그먼트에는 프로그램이 실행되면서 사용하는 데이터가 적재 됨

  CPU는 이곳의 데이터를 읽을 수 있어야 하며, 따라서 이 영역에는 읽기 권한이 부여됨

  반면 이 영역의 데이터는 실행 대상이 아니므로 실행 권한은 부여되지 않음

실행 중인 프로세스의 메모리가 사진과 같이 5개의 영역으로 구분됨

 

2. 코드 세그먼트

- 코드 세그먼트(Code Segment)는 실행 가능한 기계 코드가 위치하는 여역으로 텍스트 세그먼트(Text Segment)라고도 불림

- 프로그램이 동작하려면 코드를 실행할 수 있어야 하므로 이 세그먼트에는 읽기 권한과 실행 권한이 부여

- 반면 쓰기 권한이 있으면 공격자가 악의적인 코드를 삽입하기가 쉬워지므로, 대부분의 현대 운영체제는 이 세그먼트에 쓰기 권한을 제거함

int main() { return 31337; }

정수 31337을 반환하는 main 함수가 컴파일되면

554889e5b8697a00005dc3이라는 기계 코드로 변환되는데, 이 기계 코드가 코드 세그먼트에 위치하게 됨

 

3. 데이터 세그먼트

- 데이터 세그먼트(Data Segment): 컴파일 시점에 값이 정해진 전역 변수 및 전역 상수들이 위치

- CPU가 이 세그먼트의 데이터를 읽을 수 있어야 하므로, 읽기 권한이 부여

- 데이터 세그먼트는 쓰기가 가능한 세그먼트와 쓰기가 불가능한 세그먼트로 다시 분류됨

  > data 세그먼트: 쓰기가 가능한 세그먼트는 전역 변수와 같이 프로그램이 실행되면서 값이 변할 수 있는 데이터들이 위치

  > rodata(read-only data) 세그먼트: 쓰기가 불가능한 세그먼트에는 프로그램이 실행되면서 값이 변하면 안되는 데이터들이 위치

  전역으로 선언된 상수가 여기에 포함

int data_num = 31337;                       // data
char data_rwstr[] = "writable_data";        // data
const char data_rostr[] = "readonly_data";  // rodata
char *str_ptr = "readonly";  // str_ptr은 data, 문자열은 rodata
int main() { ... }

위는 데이터 세그먼트에 포함되는 여러 데이터의 유형임

주의 깊게 살펴봐야 할 변수는 str_ptr임

이는 "readonly"라는 문자열을 가리키고 있는데, 이 문자열은 상수 문자열로 취급되어 rodata에 위치하며,

이를 가리키는 str_ptr은 전역 변수로서 data에 위치

 

4. BSS 세그먼트

- BSS 세그먼트(BSS Segment, Block Started By Symbol Segment): 컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 메모리 영역

  > 여기에는 개발자가 선언만 하고 초기화하지 않은 전역변수 등이 포함됨

- 이 세그먼트의 메모리 영역은 프로그램이 시작될 때, 모두 0으로 값이 초기화

  > 이런 특성 때문에 C 코드를 작성할 때, 초기화되지 않은 전역 변수의 값은 0이 됨

- 해당 세그먼트에는 읽기 권한 및 쓰기 권한이 부여됨

int bss_data;

int main() {
  printf("%d\n", bss_data);  // 0
  return 0;
}

초기화되지 않은 전역 변수인 bss_data가 BSS 세그먼트에 위치하게 됨

 

5. 스택 세그먼트

- 스택 세그먼트(Stack Segment): 프로세스의 스택이 위치하는 영역

- 함수의 인자나 지역 변수와 같이 임시 변수들이 실행 중에 여기에 저장

- 스택 세그먼트는 스택 프레임(Stack Frame)이라는 단위로 사용됨

- 스택 프레임은 함수가 호출될 때 생성되고, 반환될 때 해제됨

  > 그런데 프로그램의 전체 실행 흐름은 사용자의 입력을 비롯한 여러 요인에 영향을 받음

  ex) 아래 코드에서 유저가 입력한 choice에 따라 call_true()가 호출될 수도, call_false()가 호출될 수도 있음

- 따라서 어떤 프로세스가 실행될 때, 이 프로세스가 얼마 만큼의 스택 프레임을 사용하게 될 지를 미리 계산하는 것은 일반적으로 불가능함

- 그래서 운영체제는 프로세스를 시작할 때 작은 크기의 스택 세그먼트를 먼저 할당해주고, 부족해 질 때마다 이를 확장해줌

- 스택에 대해서 '아래로 자란다'라는 표현을 사용하는데, 이는 스택이 확장될 때 기존 주소보다 낮은 주소로 확장되기 때문

- 이 영역에는 CPU가 자유롭게 값을 읽고 쓸 수 있어야 하므로, 읽기와 쓰기 권한이 부여됨

void func() {
  int choice = 0;
  scanf("%d", &choice);
  if (choice)
    call_true();
  else
    call_false();
  return 0;
}

위 코드에서는 지역변수 choice가 스택에 저장되게 됨

 

6. 힙 세그먼트

- 힙 세그먼트(Heap Segment): 힙 데이터가 위치하는 세그먼트

- 스택과 마찬가지로 실행 중에 동적으로 할당될 수 있으며, 리눅스에서는 스택 세그먼트와 반대 방향으로 자람

- C언어에서 malloc(), calloc() 등을 호출해서 할당받는 메모리가 이 세그먼트에 위치하게 되며, 일반적으로 읽기와 쓰기 권한이 부여

int main() {
  int *heap_data_ptr =
      malloc(sizeof(*heap_data_ptr));  // 동적 할당한 힙 영역의 주소를 가리킴
  *heap_data_ptr = 31337;              // 힙 영역에 값을 씀
  printf("%d\n", *heap_data_ptr);  // 힙 영역의 값을 사용함
  return 0;
}

위의 예제 코드는 heap_data_ptr에 malloc()으로 동적 할당한 영역의 주소를 대입하고, 이 영역에 값을 씀

heap_data_ptr은 지역변수이므로 스택에 위치하며, malloc으로 할당받은 힙 세그먼트의 주소를 가리킴

 

💡 힙과 스택 세그먼트가 자라는 방향이 반대인 이유?

- 두 세그먼트가 동일한 방향으로 자라며, 연속된 메모리 주소에 각각 할당된다고 가정

- 이 경우, 기존의 힙 세그먼트를 모두 사용하고 나면, 이를 확장하는 과정에서 스택 세그먼트와 충돌하게 됨

- 이를 쉽게 해결하기 위해 리눅스는 스택을 메모리의 끝에 위치시키고, 힙과 스택을 반대로 자라게 함

- 이렇게 하면 힙과 스택은 메모리를 최대한 자유롭게 사용할 수 있으며, 충돌 문제로 부터도 비교적 자유롭게 됨

 

 

# 결론

세그먼트 역할 일반적인 권한 사용 예
코드 세그먼트 실행 가능한 코드가 저장된 영역 읽기, 실행 main() 등의 함수 코드
데이터 세그먼트 초기화된 전역 변수 또는 상수가 위치하는 영역 읽기와 쓰기 또는 읽기 전용 초기화된 전역 변수, 전역 상수
BSS 세그먼트 초기화되지 않은 데이터가 위치하는 영역 읽기, 쓰기 초기화되지 않은 전역 변수
스택 세그먼트 임시 변수가 저장되는 영역 읽기, 쓰기 지역 변수, 함수의 인자 등
힙 세그먼트 실행 중에 동적으로 사용되는 영역 읽기, 쓰기 malloc(), calloc() 등으로
할당 받은 메모리

 

 

# Quiz

풀이: 초기화되지 않은 영역은 BSS 세그먼트에 저장됨

 

풀이: 지역 변수와 같은 임시 변수는 스택 세그먼트에 저장됨

 

풀이: b가 "d_str"라는 문자열을 가리키고 있는데,

이 문자열은 상수 문자열로 취급되어 rodata에 위치하게 됨

 

풀이: malloc()으로 할당 받은 메모리를 가리키기 때문에 heap 세그먼트의 데이터를 가리킴

 

풀이: 전역 변수와 같이 프로그램 실행 중 값이 바뀔 수 있는 데이터는 데이터 세그먼트에 저장됨

 

풀이: 실행 중 값이 변경될 수 있는 전역 변수이므로 데이터 세그먼트에 위치

 

풀이: 실행 가능한 코드인 함수 코드가 저장되므로 코드 세그먼트에 위치

 

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!