1장에서 본 것처럼, 메모리는 현대 컴퓨터 시스템의 작동에 핵심이다. 메모리는 각각 고유한 주소를 가진 많은 바이트 배열로 구성된다. CPU는 프로그램 카운터의 값에 따라 메모리에서 명령어를 가져온다. 이러한 명령어는 특정 메모리 주소에서 추가 로딩 및 저장을 유발할 수 있다.
예를 들어, 일반적인 명령어-실행 주기는, 먼저 메모리에서 명령어를 가져온다. 그런 다음 명령어가 디코딩되어 메모리에서 피연산자를 가져올 수 있다. 명령어가 피연산자에서 실행된 후, 결과가 메모리에 다시 저장될 수 있다. 메모리 유닛은 메모리 주소 스트림만 보고, 어떻게 생성되는지 또는 무엇을 위한 것인지 알지 못한다. 따라서, 프로그램이 메모리 주소를 생성하는 방법은 무시할 수 있다. 우리는 실행 중인 프로그램이 생성한 메모리 주소 시퀀스에만 관심이 있다.
메인 메모리와 각 처리 코어에 내장된 레지스터는 유일한 범용 저장소이고, CPU가 직접 액세스할 수 있다. 메모리 주소를 인수로 사용하는 머신 명령어는 있지만, 디스크 주소를 사용하는 것은 없다. 따라서, 실행 중인 명령어와, 명령어에서 사용하는 데이터는, 이러한 직접 액세스 저장 장치 중 하나에 있어야 한다. 데이터가 메모리에 없으면, CPU가 작동하기 전에 메모리로 이동해야 한다.
각 CPU 코어에 내장된 레지스터는 일반적으로 CPU clock의 한 사이클 내에서 액세스할 수 있다. 일부 CPU 코어는 클락 틱(클락 신호 주기)당 하나 이상의 연산 속도로 명령어를 디코딩하고 레지스터 콘텐츠에 대한 간단한 연산을 수행할 수 있다. 메모리 버스의 트랜잭션을 통해 액세스 되는 메인 메모리는 같지 않다. 메모리 액세스를 완료하려면 CPU 클록의 많은 사이클이 걸릴 수 있다. 이러한 경우, 프로세서는 일반적으로 실행 중인 명령어를 완료하는데 필요한 데이터가 없으므로, 정지해야 한다. 이러한 상황은 메모리 액세스 빈도 때문에 참을 수 없다. 해결책은 일반적으로 빠른 액세스를 위해 CPU 칩에 CPU와 메인 메모리 사이에 빠른 메모리를 추가하는 것이다. 이러한 캐시는 섹션 1.5.5에서 설명했다. CPU에 내장된 캐시를 관리하기 위해, 하드웨어는 운영체제 제어 없이 자동으로 메모리 액세스 속도를 높인다.
물리적 메모리에 접근하는 상대적인 속도에만 관심이 있는 것이 아니라, 올바른 작동도 보장해야 한다. 적절한 시스템 작동을 위해, 운영체제를 사용자 프로세스의 접근으로부터 보호하고, 사용자 프로세스 간의 접근도 보호해야 한다. 하드웨어에서 이러한 보호를 제공해야하는데, 운영체제는 일반적으로 CPU와 메모리 접근 사이에 개입하지 않기 때문이다. 하드웨어는 이 생산을 여러 가지 다른 방법으로 구현하는데, 이 chapter의 전반에 걸쳐서 보여준다. 여기서는, 가능한 구현 중 하나를 설명한다.
먼저, 각 프로세스가 별도의 메모리 공간을 가지고 있는지 확인해야 한다. 프로세스별 메모리 공간을 분리하면 프로세스가 서로 보호되고, 여러 프로세스가 메모리에 로드되어 동시에 실행되도록 하는 데 필수적이다. 메모리 공간을 분리하려면, 프로세스가 액세스할 수 있는 합법적인 주소 범위를 결정하고, 프로세스가 이러한 합법적인 주소에만 액세스할 수 있도록 해야한다. 그림 9.1에서 설명한 대로, 두 개의 레지스터를 사용하여 이러한 보호를 제공할 수 있다.

base register는 가장 작은 합법적인 물리적 메모리 주소를 보유하고, limit register는 범위의 크기를 지정한다. 예를 들어, base 레지스터가 300040을 보유하고, limit 레지스터가 120900인 경우, 프로그램은 300040에서 420939(포함)까지의 모든 주소에 합법적으로 액세스할 수 있다.
메모리 공간 보호는 CPU 하드웨어가 사용자 모드에서 생성된 모든 주소를 레지스터와 비교함으로써 달성된다. 사용자 모드에서 실행되는 프로그램이, 운영체제 메모리나 다른 사용자의 메모리에 액세스하려고 시도하면 운영 체제에 트랩이 발생하고, 운영 체제는 이 시도를 치명적인 오류로 취급한다 (그림 9.2).

- CPU에서 현재 접근하려는 주소를 읽어온다.
- 먼저 이 주소가 base 레지스터 값 이상인지 비교
- 만약 주소가 base보다 작으면, 불법 주소 접은 오류로 간주하여 트랩 발생
- 다음으로 주소가 base+limit 값보다 작은지 확인
- 만약 주소가 base+limit 이상이면, 권한 밖의 영역에 접근하려는 것이므로 트랩 발생
- 두 조건을 모두 만족하면, CPU는 안전하다고 판단하고 그 주소에 대한 메모리 접근을 정상적으로 진행
이 방식은 사용자 프로그램이 운영체제나 다른 사용자의 코드나 데이터 구조를 수정하는 것을 방지한다.
base 및 limit 레지스터는 특수 권한 명령을 사용하는 운영체제에서만 로드할 수 있다.
privileged instruction(특권 명령)
: CPU가 kernel 모드일 때만 CPU에서 실행될 수 있는 기계어 명령, 대부분 하드웨어의 민감한 부분을 건드리는 명령 ex)입출력 컨트롤, 타이머 관리, 인터럽트 관리, 유저 모드 전환 등
특권 명령은 커널모드에서만 실행될 수 있고, 운영체제만 커널 모드에서 실행되므로, 운영체제만 base 및 limit 레지스터를 로드할 수 있다. 이 방식을 사용하면, 운영체제가 레지스터 값을 변경할 수 있지만, 사용자 프로그램이 레지스터의 내용을 변경하는 것을 방지할 수 있다.
커널 모드에서 실행되는 운영 체제는, 운영 체제 메모리와 사용자 메모리에 대한 제한이 없는 액세스가 제공된다. 이 조항을 통해 운영 체제는 사용자 프로그램을 사용자 메모리에 로드하고, 오류가 발생하는 경우 해당 프로그램을 덤프하고, 시스템 호출의 매개변수를 액세스하고 수정하고, 사용자 메모리와 I/O를 수행하고, 다른 많은 서비스를 제공할 수 있다. 예를 들어, 멀티 프로세싱 시스템의 운영 체제는 컨텍스트 스위치를 실행하여, 다음 프로세스의 컨텍스트를 메인 메모리에서 레지스터로 로딩하기 전에, 한 프로세스의 상태를 메인 메모리에 저장해야 한다고 가정해 보겠다.
일반적으로, 프로그램은 이진 실행 파일로 디스크에 존재한다. 실행하기 위해, 프로그램을 메모리로 가져와서, 프로세스 컨텍스트 내에 배치해야 한다. 그러면 사용 가능한 CPU에서 실행할 수 있다. 프로그램이 실행되면, 메모리에서 명령어와 데이터에 액세스한다. 결국, 프로세스가 종료되고, 해당 메모리는 다른 프로세스에서 사용할 수 있도록 회수된다.
대부분의 시스템은 사용자 프로세스가 물리적 메모리의 어느 부분에나 존재할 수 있도록 허용한다. 따라서, 컴퓨터 주소 공간이 00000에서 시잘할 수 있지만, 사용자 프로세스의 첫 번째 주소는 00000일 필요가 없다. 나중에 운영 체제가 실제로 프로세스를 물리적 메모리에 배치하는 방법을 살펴본다.
대부분의 경우, 사용자 프로그램은 실행되기 전에 여러 단계를 거친다. 그 중 일부는 선택 사항일 수도 있다(그림 9.3).

이러한 단게에서 주소는 여러가지 방법으로 표현될 수 있다. 소스 프로그램의 주소는 일반적으로 기호적이다. 컴파일러는 일반적으로 이러한 기호 주소를 재배치 가능한 주소에 바인딩한다. 링커 또는 로더는 다시 재배치 가능한 주소를 절대 주소에 바인딩한다. 각 바인딩은 한 주소 공간에서 다른 공간으로의 매핑이다.
고전적으로, 명령어와 데이터를 메모리 주소에 바인딩하는 작업은 진행 중인 모든 단계에서 수행할 수 있다.
이 chapter의 주요 부분은 이러한 다양한 바인딩을 컴퓨터에서 효과적으로 구현하는 방법을 보여주고, 하드웨어 지원에 대해 논의하는 것을 주제로 다룬다.
(논리적 주소 공간 대 물리적 주소 공간)
CPU에서 생성되는 주소는 일반적으로 logical address라고 하며, 메모리 유닛에 의해 보이는 주소, 즉 메모리의 메모리 주소 레지스터에 로드된 주소는 일반적으로 physical address라고 불린다.
컴파일 또는 로드 타임에 주소를 바인딩하면 동일한 논리적 주소와 물리적 주소가 생성된다. 그러나, 실생 시간 주소 바인딩 방식은 논리적 주소와 물리적 주소가 다르다. 이 경우, 일반적으로 논리적 주소를 가장 주소라고 한다. 여기서 논리적 주소와 가상 주소를 같은 의미로 사용한다. 프로그램에서 생성한 모든 논리적 주소 집합은 논리적 주소 공간이다. 이 논리적 주소에 대응하는 모든 물리적 주소의 집합은 물리적 주소 공간이다. 따라서 실행 시간 주소 바인딩 방식에서는 논리적 주소 공간과 물리적 주소 공간이 다르다.
가상 주소에서 물리적 주소로의 런타임 매핑은 memory-management unit(MMU)라는 하드웨어 장치에 의해 수행된다 (그림 9.4).

섹션 9.2에서 섹션 9.3까지 논희했듯이, 이러한 매핑을 달성하기 위해 다양한 방법 중에서 선택할 수 있다. 당장은, 섹션 9.1.1에서 설명한 base 레지스터 체계를 일반화한 간단한 MMU 방식으로 이 매핑을 설명한다. base 레지스터를 이제 relocation 레지스터라고 한다. relocation 레지스터의 값은 주소가 메모리로 전송될 때 사용자 프로세스가 생성한 모든 주소에 추가된다 (그림 9.5).

예를 들어, base가 14000에 있는 경우, 사용자가 위치 주소 0을 주소로 지정하려는 시도는 동적으로 위치 14000에 재배치된다. 위치 346에 대한 액세스는 위치 14346으로 매핑된다.
사용자 프로그램은 실제 물리 주소에 절대 액세스하지 않는다. 프로그램은 위치 346에 대한 포인터를 생성하고, 메모리에 저장하고, 조작하고, 다른 주소와 비교할 수 있다. 모두 숫자 346으로. 메모리 주소로 사용될 때만 base 레지스터에 생대적으로 재배치된다. 사용자 프로그램은 논리 주소를 처리한다. 메모리 매핑 하드웨어는 논리 주소를 물리 주소로 변환한다. 이러한 형태의 실행 시간 바인딩은 섹션 9.1.2에서 논의했다. 참조된 메모리 주소의 최종 위치는 참조가 이루어질 때까지 결정되지 않는다. (⇒ 접근하는 순간에 정해짐)
이제 두 가지 다른 유형의 주소를 가진다. 논리적 주소(0에서 max)와 물리적 주소(기본 값이 R인 경우, R+0에서 R+max)이다. 사용사 프로그램은 논리적 주소만 생성하고, 프로세스가 0에서 max까지의 메모리 위치에서 실행된다고 생각한다. 그러나, 이러한 논리적 주소는 사용되기 전에 물리적 주소에 매핑되어야 한다. 논리적 주소 공간이 별도의 물리적 주소 공간에 바인딩된다는 개념은 적절한 메모리 관리의 핵심이다.
지금까지의 논의에서, 프로세스가 실행되려면 전체 프로그램과 프로세스의 모든 데이터가 물리적 메모리에 있어야 했다. 따라서 프로세스의 크기는 물리적 메모리 크기로 제한되었따. 더 나은 메모리 공간 활용도를 얻으려면, dynamic loading을 사용할 수 있다. 동적 로딩을 사용하면, 루틴은 호출될 때까지 로드되지 않는다. 모든 루틴은 재배치 가능한 형식으로 디스크에 보관된다. 메인 프로그램이 메모리에 로드되고 실행된다. 루틴이 다른 루틴을 호출해야 하는 경우, 호출하는 루틴은 먼저 다른 루틴에 로드되었는지 확인한다. 로드되지 않은 경우 재배치 가능한 연결 로더가 호출되어 원하는 루틴을 메모리에 로드하고, 이 변경 사항을 반영하도록 프로그램의 주소테이블을 업데이트한다. 그런 다음 제어가 새로 로드된 루틴으로 전달된다.
동적 로딩의 장점은 루틴이 필요할 때만 로드된다는 것이다. 이 방법은 오류 루틴과 같이 드물게 발생하는 경우를 처리하기 위해 많은 양의 코드가 필요할 때 특히 유용하다. 이러한 상황에서, 전체 프로그램 크기는 크지만, 사용되는 부분은 훨씬 작을 수 있다.
동적 로딩은 운영체제의 특별한 지원이 필요하지 않다. 이러한 방법을 활용하도록 프로그램을 설계하는 것은 사용자의 책임이다. 그러나, 운영체제는 동적 로딩을 구현하는 라이브러리 루틴을 제공하여 프로그래머를 도울 수 있다.
Dynamically linked libraries(DLLs)는 프로그램이 실행될 때 링크되는 시스템 라이브러리이다. 일부 운영체제는 static linking만 지원하며, 이 경우 시스템 라이브러리는 다른 객체 모듈과 마찬가지로 취급되며, 로더에 의해 바이너리 프로그램 이미지로 결합된다. 반면에, 동적 링크는 동적 로딩과 유사하다. 하지만, 여기서는, 로딩이 아닌 링크가 실행 시간까지 연기된다. 이 기능은 일반적으로 표준 C언어 라이브러리와 같은 시스템 라이브러리와 함께 사용된다. 이 기능이 없으면, 시스템의 각 프로그램은 실행 이미지에 언어 라이브러리의 사본을 포함해야 한다. 이 요구사항은 실행 이미지의 크기를 늘릴 뿐만 아니라 메인 메모리를 낭비할 수도 있다. DLLs의 두 번째 장점은 이러한 라이브러리를 여러 프로세스에서 공유할 수 있으므로, 메인 메모리에 DLL 인스턴스가 하나만 있다. 이러한 이유로 DLLs는 공유 라이브러리라고도 하며, Windows 및 Linux 시스템에서 널리 사용된다.
프로그램이 동적 라이브러리에 있는 루틴을 참조할 때, 로더는 DLL을 찾아서 필요한 경우 메모리에 로딩한다. 그런 다음 동적 라이브러리의 함수를 참조하는 주소를 DLL이 저장된 메모리 위치로 조정한다.
동적으로 링크된 라이브러리는 라이브러리 업데이트로 확장될 수 있따. 또한, 라리브러리는 새 버전으로 대체될 수 있으며 라이브러리를 참조하는 모든 프로그램은 자동으로 새 버전을 사용한다. 동적 링크가 없다면, 이러한 모든 프로그램은 새 라이브러리에 액세스하기 위해 다시 연결되어야 한다. 프로그램이 실수로 새롭고 호환성이 없는 라이브러리의 버전을 실행하지 않도록, 버전 정보가 프로그램과 라이브러리에 모두 포함된다. 라이브러리의 여러 버전이 메모리에 로드될 수 있고, 각 프로그램은 버전 정보를 사용하여 사용할 라이브러리 사본을 결정한다. 사소한 변경 사항이 있는 버전은 동일한 버전 번호를 유지하는 반면, 주요 변경 사항이 있는 버전은 번호가 증가한다. 따라서, 새 라이브러리 버전으로 컴파일된 프로그램만 호환되지 않는 변경사항의 영향을 받는다. 새 라이브러리가 설치되기 전에 연결된 다른 프로그램은 이전 라이브러리를 계속 사용한다.
동적 로딩과 달리, 동적 링크와 공유 라이브러리는 일반적으로 운영 체제의 도움이 필요하다. 메모리의 프로세스가 서로 보호되는 경우, 운영체제는 필요한 루틴이 다른 프로세스의 메모리 공간에 있는지 확인할 수 있는 유일한 엔티티이거나 여러 프로세스가 동일한 메모리 주소에 액세스하도록 허용할 수 있는 엔티티이다. 섹션 9.3.4에서 페이징을 논의할 때, 이 개념과 DLLs이 여러 프로세스에서 어떻게 공유될 수 있는지에 대해 상세히 설명한다.