x86 프로세서의 구조에서 프로그램을 실행하는 과정

위 이미지는 가상 마이크로컴퓨터의 기본적인 설계도이다.
여기서 계산과 논리 연산이 이루어지는 중앙 처리 장치CPU는 레지스터라고 하는 몇몇 장치들을 포함하고 있다.

먼저 clock을 살펴보면 다른 시스템 구성요소를 cpu의 내부와 동기화시키는 기능을 한다.
제어장치 CU는(control unit) 기계 명령어를 실행하는 데 수반되는 단계들의 순서를 조정한다.
산술 논리 장치 ALU는(arithmetic logic unit) 덧셈, 뺄셈, AND, OR, NOT 과 같은 연산을 수행한다.

다음으로 메모리 저장 장치는 컴퓨터 프로그램이 실행되는 동안 명령어와 데이터를 저장하는 곳이다.
이곳은 CPU에서 데이터에 대한 요청을 받으면 RAM과 상호작용한다. 주로 메모리에 있는 프로그램이 CPU에 복사되어 실행된다. 

여기서 전체적인 흐름(cpu와 BUS 작동)은 아까 말한 clock cpu에서 일정한 속도로 진동하여 내부 클럭에 동기화된다. 이런 사이클은 하나의 작동 시간을 측정한다. 보통 이벤트의 trigger로 사용된다.

clock cycle

 

 

cpu와 메모리 저장 장치는 다음과 같은 사이클을 갖는다.

명령어 실행 사이클

프로그램은 실행되기 전에 메모리의 명령어 큐queue에 적재된다. 여기서 명령어를 실행하려면 fetch, decode, execute 의 작업이 필요하다.

여기서 인출(fetch) 작업은 제어장치의 명령어 큐에서 다음 명령어를 인출하고 명령어 포인터IP를 증가시킨다. IP는 프로그램 카운터PC라고도 한다.

해독(decode)작업은 제어장치가 해야 할 명령어를 해독한다. 입력된 명령어를 ALU로 전달하고 해독하여 어떤 연산을 수행해야 할지 알려주는 신호를 ALU에 보낸다.

마지막으로 실행 작업은 ALU가 각 내부 레지스터를 피연산자로 사용하여 명령어를 실행하고 출력을 레지스터나 메모리로 보낸다. 여기서 ALU는 프로세서 상태에 대한 정보를 제공하는 상태 flags를 갱신한다.

 

 

이렇게 입력된 명령어는 cpu와 메모리 사이에서 다음과 같은 과정을 거친다.

메모리 읽기

이 그림은 일정한 간격으로 상승, 하강하는 메모리 읽기 동안의 각 프로세서 클럭 사이클이다.

cpu는 피연산자가 메모리에서 인출될 때까지 하나 이상의 클럭 사이클을 기다려야 현재 명령어의 실행을 완성할 수 있다. 여기서 낭비되는 clock cycle을 대기상태라고 한다.

첫 번째 사이클에선, 메모리의 피연산자 주소 비트가 주소 버스ADDR에 놓인다.
두 번째 사이클은 값을 읽을 것이라고 메모리에 알려 주기 위해 읽기신호RD가 낮은 값0으로 설정된다.
세 번째에선, cpu가 한 사이클을 대기하여 메모리가 응답할 시간을 준다. 이 사이클 동안 메모리 제어기는 피연산자를 데이터버스DATA에 놓는다.
마지막 사이클은 cpu가 데이터 버스에 있는 데이터를 읽었다는 것을 알리기 위해 읽기 신호를 1로 가게한다.

 

 

지금까지 컴퓨터의 기본적인 설계도를 알아봤다.
이제 IA-32 마이크로컴퓨터의 구성요소를 알아볼건데, 그 전에 알아야 하는 개념이 있다.

캐시 메모리

  • 일반적인 메모리는 cpu보다 많이 느리기 때문에 컴퓨터는 최근에 사용된 명령어와 데이터를 캐시 메모리에 저장한다.
  • 프로그램이 블록 데이터를 처음 읽을 때에 캐시에 복사본을 남겨둔다. 프로그램이 같은 데이터를 다시 찾는다면 캐시에 있는 데이터를 보게 된다.
  • cache hit은 데이터가 캐시에 있다는 것을 나타내며 cache miss는 데이터가 캐시에 없어서 일반 메모리로 읽어야 한다는 것을 나타낸다.

 

 

이제 Intel Architecture 32-bit( IA-32 ) 의 구성요소들을 알아보자.
(
인텔의 x86 아키텍처의 32비트 버전)

1. motherboard (주기판)

다른 하드웨어 구성 요소들이 서로 상호 작용하고 통신할 수 있도록 연결하는 회로 기판이다. 일반적으로 주기판은 CPU(중앙 처리 장치), RAM(랜덤 액세스 메모리), 그래픽 카드, 저장 장치(하드 드라이브 또는 SSD), 확장 카드 슬롯(PCI, PCIe 등), BIOS(기본 입출력 시스템) 를 연결할 수 있는 다양한 포트 및 연결 핀들을 포함하고 있다.

 

2. 메모리

위의 주기판에서 말한 RAM과 같은 여러개의 기본 유형 메모리들이 있다.

  • Rom (read only memory) - 영구적으로 칩에 구워지며 삭제될 수 없다.
  • EPROM (erasable programmable read only memory) - 자외선으로 느리게 지워지며 다시 프로그램 될 수 있다.
  • Dynamic Ram (DRAM) - 일반적으로 주메모리로 알려졌으며 프로그램이 실행 중일 때 프로그램과 데이터가 여기에 저장된다. 사용 시 cost는 싸지만 1msec보다 작은 시간 내에 재생되어야 하며 그렇지 않으면 저장된 내용이 손실된다. 계속 refresh되어야 한다.
  • SRam (static Ram) - 값비싼 고속 캐쉬 메모리용으로 사용된다. refresh될 필요가 없다.
  • VRam (video Ram) - 비디오 데이터를 저장한다. VRAM은 이중 포트를 갖고 있어서 하나의 포트는 계속 화면을 재생하게 하고 다른 포트는 화면에 표시할 데이터를 쓰는 것으로 video refresh를 최적화 상태로 유지한다.
  • CMOS Ram ( complimentary metal-oxide semiconductor )- 시스템 설정 정보를 저장한다. 이 메모리는 컴퓨터 전원이 나갈 때 배터리로 전원공급이 이루어지기 때문에 저장된 내용이 유지된다.

 

3. 입출력 포트

위 주기판의 BIOS 부분이다.
USB 범용 직렬 버스(universal serial bus)와 Parallel, Serial 세 종류가 있다.

  • USB 포트는 장치와 빠른 속도의 연결을 갖는다. 여러 장치에 USB 허브를 연결시킬 수 있는 복합 장치이다.
  • Parallel은 뭐.. 이름 그대로 병렬 포트를 사용하여 컴퓨터와 연결된다.
    여기서 병렬이란 뜻은 데이터 비트들을 동시에, 컴퓨터에서 독립된 선에 연결된 장치로 전달되는 것을 말한다.
    케이블 선은 짧지만 빠른 속도를 갖고 있다. 흔히 프린터기에 사용되며, 서로 양방향으로 데이터를 전송할 수 있다. (주로 실험실 기기나 맞춤형 하드웨어 장치에 대한 고속 연결에 유용함)
  • Serial중 RS-232 직렬 포트는 2진수 비트들을 한 번에 하나씩 보내서 병렬과 USB보단 훨씬 느리지만 먼 거리까지 전송할 수 있는 기능이 있고, 어셈블리 언어로 프로그래밍 가능하다.

 

4. Device Interfaces

솔직히 굳이 중요한가 싶지만.. 나와있으니까 뭐.. 일단 SATA, Bluetooth, Wi-Fi 3가지가 있는데 SATA에 대해 알아보자.

SATA는 serial ATA인데, ATA( advanced technology attachment )는 컴퓨터를 하드 드라이브와 CD-ROM과 같은 대용량 저장장치에 연결하는 IDE이다. SATA는 ATA나 IDE를 대신하여 랩톱 및 데스크톱 컴퓨터에 가장 보편적인 저장장치 인터페이스이다.

 

 

이렇게 IA-32의 구성요소를 알아봤으니 이제 어떻게 동작하는지 알아볼 차례이다.

프로그램 실행 방법

우선 운영체제는 프로그램의 현재 디스크 디렉토리에서 파일 이름을 검색한다. 만약 운영체제가 거기서 파일 이름을 찾을 수 없으면 미리 정해놓은 목록의 디렉토리에서 파일 이름을 찾는다.(주로 경로path라고 함) 그래도 못찾으면 오류다.

운영체제가 프로그램 파일을 찾았으면 디스크 디렉토리에서 파일 크기디스크 드라이브에서의 물리적 위치를 포함하는 프로그램 파일에 대한 기본 정보를 얻는다.

물리적 위치를 얻었으니, 운영체제는 메모리의 사용 가능한 다음 위치를 결정하여 프로그램 파일을 메모리로 적재한다. OS는 프로그램에 메모리 블록을 할당하고 프로그램 크기와 위치에 대한 정보를 테이블에 넣는다. 

이제 운영체제는 프로그램의 첫 번째 기계어 명령어의 실행을 시작하게 한다. 프로그램이 실행하면 그것을 프로세스라고 부르고, 운영체제는 프로세스에 PID(프로세스 식별번호)를 부여하여 실행 동안 프로세스를 추적한다.

더보기

작업관리자를 키면 프로그램들이 실행중인 모습을 볼 수 있는데 옆에 PID도 확인할 수 있다.

프로세스는 알아서 자기 코드 내의 명령들을 실행하고, 운영체제는 그 프로세스의 실행을 추적하여 시스템 자원에 대한 요청을 응답한다. (메모리, 디스크 파일, 입출력 장치)

프로세스가 끝나면 프로세스는 메모리에서 제거된다.

 

 

여기까지 대략 프로세스가 어떻게 적재되고 실행되는지 이해했다.
이러한 프로세스가 동작하는 데는 보호모드, 실제 주소 모드, 시스템 관리 모드의 세 가지 동작 모드를 갖는다. 추가로 virtual-8086라고 하는 보조 모드는 보호 모드에서 특별한 경우 동작한다. 간단하게 알아보자.

  • 실제 주소 모드 - 다른 모드로 전환하는 능력같은 몇 개의 추가적인 특징을 가진 인텔 8086 프로세서의 프로그래밍 환경을 구축한다. 시스템 메모리와 하드웨어 장치에 직접 접근할 필요가 있는 MS-DOS 프로그램을 실행하는 데에 사용될 수 있다.
  • 보호모드 - 모든 명령어와 특징을 사용할 수 있는 프로세서의 고유한 상태.
    프로그램은 세그먼트라고 하는 분리된 메모리 영역이 주어지며 프로세서는 지정된 세그먼트 바깥의 메모리 참조를 방지한다.
  • 가상 8086 모드 - 보호 모드에서 프로세서는 아까 말한 MS-DOS 프로그램과 같은 실제 주소 모드 소프트웨어를 안전한 멀티태스킹 환경에서 직접 실행할 수 있다.
    즉, MS-DOS 프로그램에 이상이 있거나 데이터를 시스템 메모리 영역에 쓰려고 할 때에 동시에 수행되는 다른 프로그램에 영향을 주지 않는다. (여러개의 가상-8086 세션을 동시에 수행 가능 )
  • 시스템 관리 모드 - 운영체제에게 전원 관리와 시스템 보안과 같은 기능을 구현하는 방법을 제공한다.
    특별한 시스템 상태로 설정하려고 하는 컴퓨터 제조사가 구현한다.

 

 

이러한 동작 모드 아래에서 기본적인 실행 환경을 알아보자.

32비트 보호 모드에서 task나 프로그램은 선형 주소 공간의 주소를 4GB까지 지정할 수 있다. 여기에 물리 주소 확장PAE라고 하는 방법을 사용해서 64GB의 물리적 메모리에 주소를 지정할 수 있다.

반면에 20비트 실제 주소 모드 프로그램은 1MB 범위만 주소를 지정할 수 있다.

그래서 프로세서가 보호 모드에 있고 가상 8086 모드에서 여러 개의 프로그램을 실행한다면 각 프로그램은 자신의 1MB 메모리 영역을 갖는다.

 

여기서 프로그램은 실행될 때 레지스터를 사용한다.
레지스터는 CPU 바로 안에 있는 고속 저장 장소이며 일반 메모리보다 훨씬 빠른 속도로 접근할 수 있다.

위의 그림을 보면 8개의 범용 레지스터와 오른쪽에 6개의 세그먼트 레지스터, 프로세서 상태 플래그 레지스터 EFLAGS와 명령어 포인터 EIP가 있다.

그 중 범용 레지스터는 계산과 데이터 전송에 주로 사용된다. 

EAX 레지스터의 하위 16비트는 위 그림과 같이 AX로 참조될 수 있다.

EAX와 같이 EㅁX 형태의 다른 범용 레지스터도 가운데 알파벳에 따라 각각 적절하게 참조 될 수 있다.

 

그 외에 다른 형태의 범용 레지스터는 이와 같이 32비트 또는 16비트 이름을 사용하여 접근할 수 있다.

 

이 중 몇 개의 범용 레지스터는 각각 특별한 용도를 갖는다.

  • EAX는 곱셈과 나눗셈 명령어에서 사용된다.
  • ECX는 CPU에서 루프 카운터로 사용한다.
  • ESP는 (시스템 메모리 구조인) 스택에 있는 데이터의 주소를 지정한다.
    특이하게 보통의 계산과 데이터 전송에는 거의 사용되지 않는다. ESP의 의미는 확장 스택 포인터 extended stack pointer.
  • ESI랑 EDI는 고속 메모리 전송 명령어에 사용된다.
    확장 소스 인덱스 extended source index, 확장 목적지 인덱스 extended destination index 의 줄임말.
  • EBP는 고급언어에서 스택에 있는 함수 매개 변수와 지역 변수를 참조하기 위해 사용된다.
    고급 수준의 프로그래밍 이외에 일반적인 계산과 데이터 전송에 사용되지 않는다.
    확장 프레임 포인터 extended frame pointer의 줄임말.
더보기

참고로
CS는 code segment
DS는 data segment
SS는 stack segment
ES, FS, GS는 추가적인 세그먼트이다.

 

실제 주소 모드에서 16비트 세그먼트 레지스터는 세그먼트라고 하는 미리 할당된 메모리 영역의 시작 주소를 가리킨다.

보호 모드에서 세그먼트 레지스터는 세그먼트 서술 테이블에 대한 포인터를 갖는다. 

위에서 말한대로 누구는 프로그램 명령어(코드)를 저장하고 누구는 변수(데이터)를 저장하고 스택 세그먼트는 함수의 지역 변수와 함수 매개변수를 저장한다.

 

추가로 명령어 포인터 EIP는 실행할 다음 명령어의 주소를 포함한다.

EFLAG 레지스터는 (간단하게 플래그라고 부름) CPU의 동작을 제어하거나 CPU연산의 결과를 반영하는 개별적인 2진수 비트들로 구성되어 있다. (1일 때 설정, 0 일 때 해제 또는 리셋)

여기서 플래그에는 제어 플래그와 상태 플래그가 있다.

제어 플래그는 CPU의 동작을 제어한다.
예를 들어 제어 플래그는 모든 명령어가 실행 된 후에 CPU를 멈추게 하고, 산술 오버플로우가 감지될 때 인터럽트를 발생시키고, 가상-8086 모드로 들어가고 보호 모드로 들어가게 한다.
여기에는 방향direction과 인터럽트interrupt 플래그가 있다.

상태 플래그는 CPU가 수행하는 산술 논리 연산의 결과를 반영한다. 상태 플래그에는 다음과 같은 플래그들이 있다.

  • 캐리 플래그CF - 부호없는 산술 연산의 결과가 너무 커서 목적지에 저장할 수 없을 때에 1로 설정된다.
  • 오버플로우 플래그OF - 부호있는 산술 연산의 결과가 너무 크거나 작아서 목적지에 저장할 수 없을 때에 1로 설정된다.
  • 부호 플래그SF - 산술 논리 연산의 결과가 음수일 때 1로 설정된다.
  • 제로 플래그ZF - 산술 논리 연산의 결과가 0일 때 1로 설정된다.
  • 보조 캐리 플래그AC - 산술 연산이 8비트 피연산자에서 비트 3에서 비트 4로 캐리가 발생할 때에 1로 설정된다.
  • 패리티 플래그PF - 결과의 하위 바이트가 짝수 개의 1인 비트들을 포함한다면 1로 설정된다. 아니면 0으로 설정.
    일반적으로 데이터가 바뀌었거나 훼손 가능성이 있을 경우 오류 검사에 사용된다.

 

 

대략 프로세스가 어떻게 적재되고 실행되는지, 프로세스의 실행환경과 구조에 대해 알아봤다.
이제 IA-32(x86)에서 어떻게 메모리 관리가 이루어지는지 보자.

더보기

참고 (gpt요약)

  1. IA-32 (Intel Architecture 32-bit):
    • IA-32는 32비트 인텔 아키텍처를 가리킵니다. 이는 인텔이 1985년에 처음 출시한 80386 프로세서를 기반으로 합니다. 이 아키텍처는 32비트 주소 버스를 사용하며, 32비트 데이터 버스를 통해 데이터를 처리합니다.
    • IA-32 아키텍처는 윈도우즈, 리눅스 등 다양한 운영 체제에서 사용되었으며, 32비트 x86 프로세서들이 이 아키텍처를 기반으로 동작합니다.
  2. x86:
    • x86은 인텔의 초기 16비트 프로세서인 8086과 그 이후 모든 호환 프로세서를 가리키는 일반적인 용어입니다. 이 용어는 16비트와 32비트 모드의 인텔 프로세서를 모두 포함합니다.
    • 따라서 x86 아키텍처는 IA-32를 포함하며, 더 나아가 64비트 아키텍처인 x86-64(또는 AMD64)도 포함합니다.

요약하면, IA-32는 32비트 인텔 아키텍처를 가리키는 용어이며, x86은 인텔의 16비트 및 32비트 프로세서를 포괄하는 일반적인 용어입니다.

 

x86 프로세서는 앞에서 설명한 기본적인 동작 모드들에 따라서 메모리를 관리한다.
그래서 이런 동작들이 어떻게 메모리를 관리할 수 있는지 자세하게 알아보자.

 

 

실제 주소 모드

실제 주소 모드에서 x86프로세서는 20비트 주소를 사용하여 0부터 FFFFF범위에 있는 1MB의 메모리에 접근할 수 있다. 엔지니어들은 인텔 8086 프로세서에 있는 16비트 레지스터가 20비트 주소를 저장할 수 없었던 기본적인 문제를 해결하기 위해 세그먼트 메모리라고 하는 방식을 생각해냈다.

모든 메모리는 세그먼트라고 하는 64kb 단위로 나누어진다. 위 그림을 보면 각 세그먼트는 마지막 16진수 자리가 0인 주소에서 시작한다. 마지막 자리가 항상 0이기 때문에 세그먼트 값을 나타낼 때에 마지막 자리는 생략한다. 

이 그림은 80000에서 시작하는 세그먼트를 보여준다. 이 세그먼트에 있는 바이트에 도달하기 위해서 세그먼트의 시작 주소에 16비트 오프셋을 더한다.(0부터 FFFF까지) 예를 들어 주소 8000:0250은 80000에서 시작하는 세그먼트 내의 오프셋 250을 나타낸다. 이 주소의 선형 주소는 80250h이다.(마지막자릿수 생략된거 주의)

 

이제 20비트 선형 주소를 계산해보자.
주소address는 메모리의 한 위치를 가리키며 x86프로세서는 각 바이트가 개별적인 주소를 갖도록 한다.

실제 주소 모드에서 선형 주소(절대 주소)는 20비트이며 16진수로 0부터 FFFFF 번지의 범위를 갖는다.

프로그램은 선형 주소를 직접 사용할 수 없어서 두 개의 16비트 수(세그먼트 - 오프셋)를 사용하여 주소를 나타낸다.
{이 세그먼트와 오프셋 값은 CD, DS, ES, SS의 세그먼트 레지스터 중 하나에 위치한다.}

CPU는 세그먼트-오프셋 주소를 20비트 선형 주소로 자동으로 변환한다.

예를들어 08F1:0100의 선형 주소를 계산한다면 0901이 된다.

 

 

이러한 선형 주소는 보호 모드에서 어떻게 사용될까?

보호 모드에서 실행될 때에 프로그램의 선형 주소 공간은 4GB이며 16 진수로 0부터 FFFF FFFF까지의 주소를 사용한다.
균일 또는 다중 세그먼트 모델 중 하나를 채택하는데, 균일 세그먼트 모델은 전역 서술자 테이블GDT에 base address와 limit 하나를 서술하고 다중 세그먼트 모델은 지역 서술자 테이블LDT에 각 task나 프로그램의 base 주소와 limit를 서술하여 각각의 메모리를 가리킨다.

이렇게 각각의 프로그램은 다른 프로그램으로부터 보호되는 메모리부분에 할당된다.

멀티태스킹을 위해 설계됐으며 Linux나 MS-Windows에서 지원한다.

 

 

세그먼트 모델 외에도 CPU에서 직접 지원하는 세그먼트 방식이 있다.

x86 프로세서는 세그먼트를 페이지라고 하는 4096 바이트의 메모리 블록으로 나눌 수 있게 하는 페이징 방식을 지원한다. 

프로그램은 실행되기 전에 메모리에 적재되어야 하지만 메모리는 값이 비싸다.
반면에 디스크 저장장소는 값이 싸고 양이 많다. 우리는 이 점을 이용해서 우리는 paging을 실행한다.

task가 실행 중일 때에 만약 task 일부분이 현재 사용 중이 아니라면 디스크에 저장될 수 있다. 사용되지 않는 task의 일부분은 디스크로 페이징(스왑)된다. 이 때 현재 실행 중인 다른 페이지는 여전히 메모리에 머물러 있다.

메모리에서 페이징 되어 나간 코드를 다시 실행할 때에 (디스크로부터 페이지를 로드해야할 때) CPU가 page fault를 발생시키며 필요한 코드나 데이터를 포함하는 페이지를 다시 메모리에 적재하도록 한다.

이 말대로라면 마냥 무한대로 페이징시키면 되지 않을까 싶지만, 디스크 접근은 메모리 접근보다 훨씬 느려서 프로그램이 페이징에 더 많이 의존하면 할수록 프로그램은 더 느리게 실행한다.