본문 바로가기

공부기록/42 Seoul

C언어의 메모리 구조에 대하여

메모리 구조는 진작에 짚고 넘어갔어야할  주제인데 이제서야 제대로 공부해봤다.
libasm 과제를 하면서 어셈블리어를 처음 접했다.
주먹구구식으로 하다보니 공부할수록 찜찜하게 이해가 안 되는 부분들이 많았다ㅠㅠ
아마도 메모리 구조를 어설프게 알고 있어서 그런게 아닐까.

 

  • 어셈블리어란?

어셈블리어는 기계어와 일대일 대응이 되는 low level 언어이다. 101010001 이런 기계어와 C, Java, Python 등 high level 언어의 중간에 있다고 볼 수 있겠다.

 

  • 어셈블리어가 왜 중요하지? 왜 알아야하지?
  1. 디버깅 결과가 어셈블리어로 나오니까. 어셈블리어를 모르면 결과를 분석할 수 없다.
    gdb에서 -g 옵션을 주면 어셈블리어가 아닌 디버깅도 되긴 하지만 뭐.. 일단 gdb 잘 모르니까 넘어가고
  2. 어셈블리어를 알면 컴퓨터 시스템 내부의 동작 원리를 이해하기가 쉽다.
  3. 해커가 될 수 있다ㅋ. 소스코드가 없어도 리버스 엔지니어링을 통해 프로그램을 분석할 수 있다.

아무튼, 어셈블리어가 메모리 주소와 값에 접근하며 메모리와 밀접하게 작동하다보니 메모리 구조를 공부하지 않을 수 없었다.

 


메모리 구조

그림과 함께 우리가 실행한 프로그램이 메모리에 어떻게 올라가는지 보자.

그림 종류가 두 가지가 있다.

난 높은 주소가 위에 그려진 왼쪽 그림이 직관적이라 더 편한 것 같다.

스택에 데이터가 쌓이는 모습을 표현하기 위함 때문인지 오른쪽 그림처럼 거꾸로 그린 그림도 많다.

 

메모리 구조

 

그림에 그려져있는 부분은 유저 영역이다. 

그리고 그림엔 안 나와있지만, stack 영역 보다 더 높은 메모리 주소엔 커널영역이 있다.

커널 영역은 시스템 운영에 필요한 메모리로, OS 가 올라가있다. 사용자가 함부로 접근할 수 없는 영역이다.

 

이제 유저 영역의 각 영역들을 자세히 살펴보자.

 

1. Text Segment (=Code Segment)

  • 작성한 코드가 기계어로 번역되어 저장되는 곳이다.
  • 프로그램이 실행되면, 이 영역에 있는 코드들이 한 줄씩 실행되는 것이다.
  • 여기는 read-only 영역이라 데이터들이 변경될 수 없다.
  • rodata (read-only data)인 상수리터럴도 이 영역에 저장된다.

 

2. Data Segment

    전역변수(global), 정적변수(static), 배열(array), 구조체(structure) 등이 저장된다.
    이 영역은 read-only가 아니므로 프로그램 실행중에 값이 바뀌는 것이 가능하다

   

    2.1 Initialized Data Segment

  • 프로그래머가 직접 초기화를 해준 전역변수와 정적변수가 저장되는 곳이다.

 

    2.2 Uninitialized Data Segment

  • BSS segment (Block Started by Symbol) 라고 불린다.
  • 초기화 되지 않은 전역변수(global)와 정적변수(static)가 저장되는 곳이다.
  • 초기화 되지 않은 변수는 커널이 자동으로 0으로 초기화시킨다.

 

3. Stack

  • 스택 영역은 힙 영역과 인접해있고, 메모리 주소가 높은 곳에서 낮아지는 방향으로 자란다.
  • 함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역이다.
  • 컴파일시 크기가 결정된다.
  • 함수가 호출되면 함수의 스택 프레임(stack frame)이 생성되고, 스택 프레임이 스택에 추가 된다. (Push)
  • 함수의 실행이 끝나 함수가 반활 될 때 스택 프레임이 스택에서 제거된다. (Pop)
  • LIFO (Last In First Out) 구조를 갖는다.

 

4. Heap

  • 동적할당(malloc, relloc, free)으로 할당한 메모리들이 위치하는 영역이다.
  • 스택과 반대로 메모리 주소가 낮은 곳에서 높은 곳으로 증가한다.
  • 런타임시 크기가 결정된다.
  • 힙 영역은 라이브러리와 프로세스에서 동적으로 로드 된 모듈들끼리 공유할 수 있는 영역이다.
  • free 제대로 해주자.

스택프레임(Stack Frame)에 대하여

아래와 같은 프로그램이 실행된다고 하자. 

정수 두 개를 매개변수로 받아 그 합을 리턴하는 sum 함수이다.

int sum(int a, int b)
{
    return (a + b);
}
 
int main(void)
{
    int c = sum(12);
    return c;
}

👇 위에 스택 영역에 대해 이런 말을 써놨었다.

  • 함수가 호출되면 함수의 스택 프레임(stack frame)이 생성되고, 스택 프레임이 스택에 추가 된다. (Push)
  • 함수의 실행이 끝나 함수가 반활 될 때 스택 프레임이 스택에서 제거된다. (Pop)

위 예제 코드가 실행되면서 스택프레임이 어떻게 생성되고 제거되는지 살펴보자.

 

먼저 main 함수가 호출되면 아래와 같은 스택프레임이 생성되고 스택에 추가된다.

main() 함수의 스택프레임

스택프레임의 구성

  • RET : Return address
    함수가 종료된 후 반환 되어 돌아갈 위치를 담고 있다.
  • RBP : Base Pointer
    스택의 가장 낮은 위치를 가리킨다 (메모리에선 제일 높은 주소). 여기부터 스택이 쌓이기 시작할거라고 알려준다.
  • RSP : "스택 포인터" 레지스터. 스택에 새로운 값이 들어올 때마다(push) 스택의 top을 추적한다.
  • 로컬변수 저장을 위한 메모리 공간
  • 매개변수 저장을 위한 메모리 공간

 

다음으로 sum( ) 함수가 호출되면 sum() 함수의 스택프레임이 스택에 push 된다. 

함수가 끝나면 해당 스택프레임은 스택에서 제거되고 RET 정보에 따라 돌아가야할 위치로 돌아간다.

sum() 함수가 호출 된 후 스택의 모습

 

스택프레임의 개념을 알고나니 재귀함수의 작동원리도 더 잘 이해됐다.

함수가 이런 원리로 실행되니까

같은 함수가 계속 실행되는데 변수가 충돌하지도 않고 실행했던 함수들의 반환도 착착 이루어질 수 있는 것이었다.


 


다음엔 gdb랑 lldb로 디버깅 실습을 해볼까한다.

참조

👆 전역변수, 정적변수를 선언해 변수들의 생존 범위를 확인해보고, 지역변수를 여러개 선언한 후 %p로 메모리 주소를 출력해 stack에서 메모리주소가 낮아지는 방향으로 쌓이는지 확인해보는 예제가 있다.

 

 

Memory Layout of C Programs - GeeksforGeeks

Memory Layout of C Programs

www.geeksforgeeks.org

👆 동빈나의 시스템 해킹 강의 중 스택프레임에 대해 설명한 부분.