블로그 이미지
대갈장군

calendar

1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

Notice

2016. 2. 6. 05:55 프로그래밍/C++
출처: http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/

저자: Gustavo Duarte


요즘 메모리 관련 문제를 다루다 보니 점점 더 힙 속으로 빠져들고 있던 차에 좋은 글을 발견하고 내 개인 소장용으로 옮겨 볼까 한다.


일반적으로 멀티 테스킹 OS에서 각각의 프로세스는 그들 자신만의 메모리 샌드 박스에서 실행된다. 즉, 각각의 프로세스가 개별적인 주소 공간을 소유한다는 의미... 이 샌드 박스는 일반적으로 32비트 환경일 경우 Virtual Address Space라 부르고 약 4GB의 메모리 공간이 주어진다. (32비트니까~)


이 가상 메모리 주소 공간은 실질적인 (물질적인) 메모리와 이른바 Page Table이라는 놈을 통해 맵핑 되는데, 뭐, 쉽게 말해서 가상 주소는 말그대로 가상으로 주어진 공간이고 실제 메모리가 할당되려면 맵핑이 필요한데 이 주소록을 가지고 있는 녀석이 Page Table이라는 거지. 재미 있는 점은, 컴퓨터에 돌아가는 모든 프로세스는 이 룰에 따라야 한다는 말이다. 심지어 커널 자신 조차도... 쉽게 생각하면 커널은 예외일 것 같지만 그렇지 않다는 점을 글쓴이는 말하고 싶었던가 보다. 아무튼, 그래서 이 가상 주소 공간은 반드시 커널을 위해 일부를 쓴다는 점이다. (아래 그림처럼)

Kernel/User Memory Split

리눅스는 기본적으로 1GB만 커널이 사용하도록 기본 셋팅이 되지만 윈도우는 2GB나 커널을 위해 할당한다. 즉, 프로세스 자신은 남은 2GB만 사용가능하다는 거네... 이런 위도우 같은 경우를 봤나... 물론 윈도우도 3GB까지 확장하는 옵션이 있단다... 휴..


뭐 이쯤에서 눈치 챘겠지만 각 프로세스가 4GB씩이나 할당하면 나의 구린 컴퓨터는 고작 16GB 램인데 프로세스 4개 돌리면 램 바닥찍고 끝나는가 라는 의문이 스멀스멀 든다면 4 곱하기 4는 16이므로 정답..


물론, 각각의 프로세스가 4GB를 무조건 쓴다는 건 아니라는 점... 시작은 미미하게 해서 끝은 창대해 질수 있겠지만 일반적으로 시작할때는 최소 필요 메모리만 할당해서 최대 4GB까지 쓸수 있다는 점을 이론적으로 보여주는 것일 뿐, 실제로 프로세스가 무조건 4GB 할당하고 출발하는 건 아니라는 점.


다만, 프로그래머가 알아야 할 것은 실수로 커널 메모리 영역을 침범하면 운영체제가 "네 이놈! 여기가 어디라고 감히 들이대느냐!!" 하면서 프로그램을 종료시켜 버린다는 거... 또 한가지 중요한 점은, 커널 코드와 데이터는 항상 어디서나 접근 가능하고 인터럽트를 핸들링 하며 시스템 콜을 받아들일 준비가 되어 있는 반면, 사용자 주소 영역은 프로세스가 변경되면 그에 따라 변화한다는 점... (아래 그림)

Process Switch Effects on Virtual Memory

위 그림에서 사용자 주소 영역에 하늘색 표신 된 부분은 메모리가 할당되어 사용되어진 영역이고 흰색은 할당되지 않은 영역이다. 재미 있는 건, 저자가 Firefox는 메모리를 무지하게 잡아 먹는다면서 저렇게 Firefox의 메모리 할당을 크게 그렸다는 점... 크롬이 더 많이 먹는거 아닌가 몰라...

Flexible Process Address Space Layout In Linux

위 그림을 보면 대략적인 프로세스의 주소공간을 (4GB) 볼 수 있다. 일반적으로 모든게 제대로 잘 작동했을 경우 대부분의 프로그램은 위의 그림과 같은 구조의 메모리 공간을 가지고 시작하게 된다. 사실 이점에 대해서 저자는 보안 취약점이 될 수 있다고 지적하고 있다. 해커가 이런 메모리 구조를 미리 파악하고 대충 찍어서 맞추는 경우 보안상의 헛점이 드러날 수 있다는 점. 그래서 요즘은 메모리를 랜덤하게 유행한단다... 이 글이 써진 시점이 무려 2009년이니... 허허


위 그림에서 사용자 공간의 최상층에 위치하고 있는 것이 바로 Stack인데 이 녀석은 지역 변수를 저장하고 함수의 인자들을 저장하는 용도로 주로 사용된다. 함수를 호출하게 되면 스택영역에 Stack Frame이라는 놈을 넣게 되는데 이 함수가 리턴 될 때 이 Stack Frame도 파괴된다. 스택 자체가 LIFO 방식으로 단순하게 동작하므로 pushing과 popping이 매우 빠르고 효율적으로 수행 가능하다. 그리고 스택 영역을 자주 그리고 많이 사용하면 결국 CPU 캐시에 스택 메모리가 활성화 되어 있게 되므로 스피드를 더더욱 업업업~ 각각의 스레드는 자신만의 스택을 가진다.


만약 사용자가 스택 영역에 할당된 현재의 영역보다 더 큰 데이터를 집어 넣게 되면? 이렇게 되면 (리눅스의 경우) expand_stack()이라는 함수 (결국, acct_stack_growth() 함수 호출하는 녀석)를 호출해서 스택 영역의 확장이 가능한가 체크한다. 만약 스택 영억이 기본 최대 사이즈 (8MB)보다 작다면 스택 영역이 확장된다. 근데, 만약 이 8MB를 다 써버리면? 그때 바로 터지는 것이 Stack Overflow이고 프로그램은 Segmentation Fault를 받으면서 장렬히 전사한다. 또 한가지 흥미로운 점은 스택 영역은 한번 확장되면 스택이 줄어든다고 해서 확장된 영역을 줄이진 않는다. 이것이 마치 미국의 연방 정부의  예산과 같다고... 늘리기만 하고 줄이진 않는다는 이야기.. ㅋㅋ


자, 그 다음은 스택 아래에 위치한 Memory Mapping Segment라는 놈인데 이 놈은 파일의 내용을 직접적으로 메모리로 맵핑해준다. 이런 일을 해주는 대표적인 함수로는 mmap(), CreateFilemapping(), MapViewOfFile() 같은 함수들이다. 이러한 Memory Mapping은 고성능 파일 입출력이나 Dynamic library들을 로딩하기 위해서 사용되어 진다. 그리고 이른바 Anonymous Memory Mapping도 가능한데 이건 임의 메모리 맵핑이라 해야 하겠네.. 파일이나 라이브러리 같은걸 맵핑 하는게 아니라 malloc() 같은 함수가 제법 큰 메모리 공간을 할당하려고 할때 종종 이 구역의 메모리를 임의 메모리 맵핑이라는 형태로 사용하도록 해준다는 군. 메모리를 좀더 효율적이고 많이 사용하기 위해 그런듯...


그다음은 바로 힙! 엉덩이 아니지요.. Heap! 사실 프로그램 돌리다 보면 터지는 메모리 에러의 90% 이상이 힙에서 터진다고 봐도 과언이 아니다. 이 힙 영역은 프로그램이 런타임 (실행중) 중에 메모리를 임의의 크기로 할당할때 사용되는 영역이다. 이러한 동적 메모리 할당 때문에 힙 메모리 사용은 커널과 언어의 런타임이 서로 협상해서 만들어 진다. 대표적인 힙 할당 함수로 C에서는 malloc이 있고 C#이나 C++의 경우에는 new가 있다. 


만약 메모리 공간이 여유가 있다면 언어의 런타임에서 알아서 처리하고 끝낸다. 만약 현재의 할당된 힙으로 공간이 부족하다면 시스템 콜 (brk())을 통해 확장되는데 이것이 바로 커널의 개입이다. 사실 힙의 관리는 정말 복잡한데 머리 좋고 똑똑한 사람들이 최선을 다해 알아서 잘 관리하도록 만들어 놨다고... 뭐, 아무튼...


그리고 마지막으로 제일 아래 영역이 보이는데 BSS, data 그리고 program text다. BSS와 data store의 경우 static global variable들을 저장하는 용도로 사용된다. BSS는 할당되지 않은 (uninitialized) static variable들을 저장하고 data segment의 경우에는 initialized된 static variable들을 저장한다. 


뭐 몇가지 설명이 더 있기는 한데 그렇게 필요해 보이진 않아서 스킵~ 




posted by 대갈장군