2013. 10. 8. 01:26

Memory Segmentation I

지난 시간에 이어 메모리 세그먼테이션에 대해서 자세히 알아봅시다.

 

세그먼테이션은 x86 아키텍쳐에서 제공하는 메모리 관리 스킴 중에 하나로써, 메인 메모리 영역을 세그먼트라는 단위로 분할하여 관리합니다. 세그먼테이션을 수행한다는 의미는 하나의 세그먼트 영역의 베이스 어드레스와 오프셋으로 어떤 특정 메모리 위치를 참조하는 동작을 의미합니다. 이런 뻔한 설명을 드려도 세그먼테이션 개념이 와닿지 않는 핵심적인 의문 두가지가 있습니다. 세그먼테이션을 수행하는 주체는 무엇인가? 그리고 세그먼테이션은 언제 수행되는가? 우선 우리는 이 질문에 대답을 해보도록 합시다.

 

아래의 간단한 C언어 코드로 예를 들겠습니다.

 

unsigned int gValue;    // 데이터 세그먼트 오프셋 0x1000에 전역 변수 gValue가 위치한다고 가정

 

int main(void)
{

        gValue = 0x0000FFFF;     // 0x0000FFFF 값을 전역 변수 gValue에 대입

 

        return 0;
}

 

자, 이번에는 위의 C언어 코드를 CPU가 해석할 수 있도록 컴파일해서 저 간단한 프로그램을 실행했다고 가정하겠습니다. (사실, 의미없는 동작이기 때문에 컴파일러가 이를 최적화하지만 그냥 무시합시다)

그러면 메모리에 로딩된 프로그램은 대략적으로 아래의 어셈블리 코드로 동작할 것입니다.

 

0x4005000 MOV EAX, 0x0000FFFF

0x4005004 MOV DWORD PTR DS:[0x1000], EAX

 

1) 프로그램 카운터 0x4005000에서 명령어 MOV EAX, 0x0000FFFF을 패치하고 수행

 - EAX 레지스터에 즉치값 0x0000FFFF 대입

 

2) 프로그램 카운터 0x4005004에서 명령어 MOV DWORD PTR DS:[0x1000], EAX를 패치하고 수행

 - 데이터 세그먼트 베이스 어드레스로부터 오프셋 0x1000 위치에서 4바이트만큼 참조하여

   0x0000FFFF을 쓰기

 

            [그림1] CPU가 명령어를 실행할 때, 오퍼랜드에 기술된 메모리 위치를 참조하는 과정

 

  세그먼테이션 동작을 전적으로 수행하는 주체는 CPU가 되고, 세그먼테이션을 수행하는 시점은 CPU가 하나의 명령어를 패치, 디코딩 후 오퍼랜드의 주소를 지정하기 위해 계산하는 시점입니다. 이때, 명령어 셋에 명시된 어드레싱 모드가 Immediate Addressing이 아닌 Direct Addressing, Register Indirect Addressing, 그리고 Base Addressing 등에 해당한다면 보호 모드(Protected Mode)에서는 세그먼트 어드레싱(Segmented Addressing) 즉, 세그먼테이션을 수행하게 됩니다. 여기선 세그먼테이션을 수행하는 시점을 알게 되었습니다만, 그 구체적인 과정에 대해서는 여전히 언급하지 않았습니다. 다음에는 위의 그림에 나오는 세그먼테이션 과정을 설명하기 앞서, 몇 가지 이해가 필요한 개념들의 정의를 짚고 넘어 가도록 합시다.

 

 

Notes and References

------------------------------------------------------------------------------------------------------

1. http://cs.smith.edu/~thiebaut/ArtOfAssembly/CH03/CH03-3.html (Instruction Execution)

2. http://en.wikipedia.org/wiki/X86#Addressing_modes (Addressing Modes)

3. http://forum.osdev.org/viewtopic.php?f=11&t=26012&start=0 (To load a segment register, CPU operation)

4. http://www.supernovah.com/Tutorials/Assembly3.php (Addressing Modes)

5. IA-32 Intel Architecture Manual VOLUME 2A: Instruction Set Reference, A-M (Instruction Set Architecture)

------------------------------------------------------------------------------------------------------

 

오늘은 시간이 늦었으니, 짧게 마무리하고 여기서 다시 추가로 계속 설명드리도록 하겠습니다.

 

편안한 밤 보내세요.

 

 

Written by Simhyeon, Choe

 

2013. 10. 5. 01:51

x86 아키텍쳐 메모리 관리 스킴의 이해 II

  지난 시간에 이어, x86 아키텍쳐 메모리 관리 스킴에 대해서 계속 공부하도록 합시다. 지난 시간에는 세그먼테이션과 페이징의 기본 개념과 필요성에 대해서 언급하였는데요. 현재의 메모리 관리 스킴을 설명하기 앞서, 일단 과거 x86 아키텍쳐의 메모리 관리 스킴의 히스토리를 조금 짚고 넘어가도록 하겠습니다.

 

1) 세그먼트(Segment)와 오프셋(Offset)

  세그먼테이션은 고유한 속성을 가진 메모리의 조각을 영역별로 나누어 어드레싱하고 관리하는 기법이라고 이전 챕터에서 정의하였습니다. 과거 8086 아키텍쳐에서 지원하는 세그먼테이션의 경우에는 현재 세그먼테이션 스킴과 비교해 컨셉이 전혀 달랐습니다. 8086 아키텍쳐에서 사용한 세그먼테이션의 근본적인 배경은 더 많은 메모리 주소 공간을 어드레싱하기 위함이었지요. 당시 8086 아키텍쳐는 8개의 16비트 레지스터와 16비트 데이터 버스와 20비트 어드레스 버스를 가졌습니다. 하나의 16비트 레지스터로 20비트 어드레스를 표현할 수 없었기 때문에 인텔이 고안해낸 방법은 세그먼트:오프셋 조합으로 20비트 어드레스를 나타내는 것이었습니다. 그러니까 세그먼트 값을 4비트만큼 왼쪽으로 쉬프트하고 여기에 오프셋 값을 더하여 온전한 20비트의 물리 주소를 표현하였습니다.

  이와 같이, 세그먼트:오프셋 조합으로 물리 주소를 표현하면 동일한 물리 주소를 다양한 세그먼트:오프셋 조합으로 표현할 수 있습니다. (예를 들면, 0xFFFFF 물리 주소는 FFFFh:000Fh, FFF0h:00FFh, FF00h:0FFFh, F000h:FFFFh로 표현 가능) 그렇다면 만약에 FFFFh:0010h으로 물리 주소를 표현하면 어떻게 될까요? 이 경우엔 20비트로 표현할 수 있는 범위를 초과하기 때문에 다시 0x0000의 물리 주소로 랩핑됩니다.

 

2) 리얼 모드(Real Mode or Real Address Mode)와 보호 모드(Protected Mode)

  8086 머신에서는 20비트 어드레스를 표현하기 위해 세그먼트:오프셋 조합으로 주소 지정을 하게 되는데, 1MB 범위의 메모리 영역 어느 곳이든 주소 지정이 가능할 뿐만아니라, 읽기/쓰기가 가능합니다. 즉, 전적으로 프로그램을 구현하는 프로그래머가 메모리 관리를 책임져야 하며, 이는 과거 머신에서 메모리 관리에 대한 안정성과 보안성에 대한 고려가 되지 않았습니다. 이러한 CPU 연산 모드를 리얼 모드라고 합니다. 8086 이후에 등장하게 된 80286 프로세서부터 멀티 태스킹 환경을 제공했습니다. 그리고 어드레스 버스가 24비트로 확장되었으며, 더이상 이전의 세그먼트:오프셋 조합으로 주소 지정하지 않고 세그먼트 셀렉터가 가리키는 세그먼트 디스크립터의 베이스 어드레스와 오프셋의 조합으로 최대 16MB까지 주소 지정을 가능하도록 하였습니다. 이것은 제가 원래 처음에 설명드렸던 세그먼테이션 컨셉이기도 합니다. 하위 호환성 유지를 위해 여전히 리얼 모드를 제공하였고 새롭게 보호 모드를 도입하였습니다. 이것은 다른 세그먼트들간의 코드, 데이터, 스택 영역에 대한 오버랩핑을 제한하도록 하고 세그먼트마다 특권 레벨을 부여하여 합법적이지 않은 접근에 대해 각 세그먼트 영역을 보호하였습니다. 80286 다음으로 등장하게 된 프로세서가 80386으로, 이때부터 완전한 32비트 레지스터 셋과 함께 32비트 버스 라인을 가졌습니다. 여기서부터 유저 영역과 커널 영역의 주소 공간을 나누어 유저 레벨에서 동작하는 프로그램이 커널 영역의 접근을 보호하도록 하였습니다. 그리고 유저 레벨의 프로세스마다 독립적인 영역으로 분리를 통해, 서로간의 메모리 영역 침범을 제한하여 안정성과 보안성을 높였습니다. 그리고 페이징 유닛의 추가로, 유저 프로세스마다 4GB에 이르는 주소 공간에 가상 메모리를 할당할 수 있도록 지원하였습니다.

 

여기까지 정리를 마무리하도록 하겠습니다. 즐거운 하루 보내세요.

 

 

Written by Simhyeon, Choe

 

2013. 10. 3. 14:49

x86 아키텍쳐 메모리 관리 스킴의 이해 I

이번 시간에는 x86 아키텍쳐의 메모리 관리 스킴에 대해서 알아 보도록 합시다.

 

사실, 이 주제를 포스팅하는 것에 대해 고민하였습니다. 이 주제로 검색하면 굉장히 잘 정리된 블로그나 기술 문서들이 많기 때문입니다. 특히, 운영체제 개발 관련 서적에서는 이와 관련된 내용이 체계적으로 잘 정리가 되어 있습니다. 그리고 그것을 구현한 코드도 함께 볼 수 있구요. 이미 보편적으로 오래전에 파급된 이 주제를 제가 이제와서 언급하는게 적절한지는 잘 모르겠군요. 제가 x86 Architecture에 대해 공부한지는 꽤 되었습니다. 예전에 차라리 정리를 해놓았으면 하는 후회가 남네요. 뒤늦게라도 다시 정리를 해보는 것도 리마인드 차원에서 나쁘지는 않을 것 같군요. 사실 저는 개발하는 것보다 이론 공부를 더 좋아하는 성격인지라, 순전히 이론만으로 이 주제를 잘 설명할지는 의문이긴 합니다만, 이번 시간에 한 번 정리를 해보도록 하겠습니다.

 

당연히 이 주제는 너무나도 유명한 아래의 인텔 메뉴얼을 참고하였습니다.

Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A:
System Programming Guide, Part 1

 

x86 아키텍쳐에서 메모리 관리 기법은 크게 세그먼테이션과 페이징으로 분류할 수 있습니다. 이 두가지 메모리 관리 기법을 언급드리기 앞서, 우리가 선행해야 할 것은 과거의 메모리 관리 스킴입니다. 과거 메모리 관리 스킴으로 시작하여 현재 어떻게 변천해 왔는지 알아보는 것은 충분히 의미가 있습니다.

 

우선, 세그먼테이션(Segmentation)를 간단히 정의해도록 합시다. 세그먼트는 우리말로 풀어보면 사전적 의미는 '조각'이고, 세그먼테이션은 '분할'로 나오네요. 굳이 억지로 번역하면 '조각 나누기' 정도로 해석할 수 있겠네요. 그럼 필요한 이유가 뭘까요? 저의 생각으로는 적당히 세 가지 정도를 이유로 들 수 있을 것 같은데요. 첫 번째는 코드와 데이터처럼, 서로 다른 성격을 지닌 메모리 영역을 나누어 관리하기 위함입니다. 당연한 얘기가 되겠지만 프로그램을 동작중에 데이터를 코드 영역에다가 쓰기 행위는 제한되어야 할 것이고, 또 다른 예로는 프로그램 특성상 오직 읽어야 하는 영역임에도 불구하고 이를 접근하여 쓰기를 시도하는 행위도 읽기 전용 메모리 속성으로 구분되어야 할 것입니다. 이와 같이, 세그먼테이션을 통해 각 세그먼트의 보호 속성을 지정하여 관리할 수 있습니다. 두 번째는 필요에 따라서 특정 메모리 영역을 공유할 수 있습니다. 동일한 프로그램을 두 번 실행되었다고 하더라도 프로그램 코드를 두 번 메모리에 로딩할 필요없이 특정 코드 세그먼트 영역에 한 번만 로드하여 공유하고 데이터 영역만 분리하여 효율적인 메모리 관리를 할 수 있습니다. 이것은 사실 멀티 태스킹 환경에서 그러한 지원은 당연하다고 볼 수 있지요. 세 번째 이유는 특정 메모리 영역의 접근이 용이하다는 점입니다. 단순히 물리 메모리를 하나로 코드와 데이터 등의 영역을 관리한다면 유저 입장에서는 특정 영역에 접근할 때, 예를 들어 코드는 0xFFFF, 메모리는 0x1000와 같은 절대 위치를 항상 참조해야 하기 때문에 번거로움이 있습니다. 그래서 각 세그먼트 영역마다 베이스 주소와 한계 주소과 같은 정보를 CPU에게 미리 알려 준 상태라면 유저(프로그래머) 관점에서는 어떤 영역이든 오프셋 0을 기준으로 하여 접근하면 되니 메모리 관리에 신경쓰지 않아도 될 것입니다. 다시 요약하면, 속성이 다른 영역의 메모리를 적절히 나누어 관리하기 위한 기법으로 정의할 수 있습니다. 그렇다면 이번에는 페이징에 대한 정의를 찾아봐야 할 것 같습니다만, 앞서 페이징이 필요한 이유부터 알아봅시다.

   우선 세그먼테이션을 기반으로 하여 가상 메모리가 아닌 순수 물리 메모리상에서 여러 개의 프로그램이 동작하는 멀티 태스킹 환경이라고 가정하겠습니다. 그림①은 프로그램 크기가 다른 프로세스 A, B, C, D, E가 차례대로 물리 메모리 영역에 로딩된 상황입니다. 순서대로 실행하여 메모리에 로딩한 상황 자체는 특별한 문제가 없습니다. 그리고 그림②에서는 프로세스 B와 D가 실행이 끝나고 언로딩된 상황을 보여주고 있습니다. 여기서 문제는 다시 새로운 프로세스 F를 실행하기 위해 로딩하려는 시점입니다. 그림에서 보시다시피, ⓐ와 ⓑ 영역에 Process F를 연속된 공간으로 로딩하기엔 공간이 부족합니다. 이와 같이, ⓐ와 ⓑ 영역을 메모리에서 외부 단편화(External Fragmentation)가 발생하였다고 표현합니다. 외부 단편화의 범위는 프로세스가 가지고 있는 코드와 데이터 영역 모두가 됩니다. 이런 외부 단편화로 인해, 프로그램 로딩을 위한 적절한 공간을 할당받을 수 없다면 기존에 로딩한 프로그램들을 재배치하고 조각으로 나누어진 영역들을 하나로 합치는 등의 워스트 케이스가 발생하면 그 버든은 아주 클 수 밖에 없지요. 바꿔 얘기하면, 외부 단편화 문제는 세그먼테이션에서 발생하는 문제라고 말씀드릴 수 있습니다. 외부 단편화 문제를 해결하기 위해, 인텔 x86에서는 페이징 기법을 제공합니다. 프로그램을 실행할 때 코드와 데이터를 통째로 로딩하는 것이 아니라, 모든 영역을 페이지 단위(IA-32에서는 기본 4K)로 나누어 메모리를 관리할 수 있습니다.

위의 왼쪽 그림을 보면, 물리 메모리에서 각 프로세스의 코드와 데이터 일부가 페이지 단위로 로드되어 있습니다. 중간 그림에서 프로세스 D의 실행이 종료되고 페이지 아웃된 후에 새로운 프로세스 E 코드 일부가 물리 메모리에 로드된 것을 보여주고 있습니다. 페이지 단위의 메모리 공간 확보만 보장된다면 프로그램 영역 할당은 쉽게 이루어질 수 있습니다. 즉, 페이징 기법으로 외부 단편화 문제를 극복할 수 있습니다. 그리고 여기서 아직 가상 메모리 개념을 자세히 설명드리지 않았습니다만, 사실 페이징 기법은 가상 메모리 스킴과 밀접한 관련이 있습니다. 위의 그림에서는 단지 실행에 필요한 프로세스 코드와 데이터 일부가 로드된 것을 보여준다는 점을 유념하시기 바랍니다. 일단, 오늘은 여기까지만 정리하고 다음편에서 이 주제에 대해서 계속 설명드리겠습니다. 좋은 하루 보내세요.

 

 

Written by Simhyeon, Choe