추측 실행(Speculation Execution)

성능을 위해서 다음 실행될 명령어 예측하여 먼저 실행하는 기법.

분기 예측은 추측 실행의 한 종류라고 할 수 있습니다.

if (a > 16)    
    b = 32

위의 조건문에서 a > 16을 판단하기 위해서 a 값을 읽어오는데, 시간이 걸립니다. 이때 미리 b = 32를 실행해 둡니다.

예측이 맞으면 미리 실행한 만큼 성능이 좋아집니다.

 

추측 실행과 비순차적 실행으로 인해서 멜트다운 취약점이 발생합니다.

인텔 CPU와 ARM Cortex-A 시리즈 기종에서만 발생합니다

커널 메모리를 읽을 수 있는 치명적인 취약점입니다.

 

공격

raise_exception();
// the line below is never reached
access(probe_array[data * 4096]);

1. raise_exception() 함수가 실행됩니다.

2. 순차적 실행에서는 3번 줄이 실행되지 않지만, 비순차적 실행으로 인하여 3번째 줄이 실행되게 됩니다.

3. raise_exception() 함수에 의해 예외가 발생하고 제어 흐름이 운영체제의 예외 처리기로 점프합니다.

4. 예외로 인해서 순서에 맞지 않게 실행된 명령은 폐기되지 않습니다.

5. 이 예외가 메모리 액세스 또는 다른 CPU 예외로 인해 발생했는지 여부에 관계없이 제어 흐름은 다음 사용자 공간 명령이 아닌 커널에서 계속됩니다.

6. 커널에서 실행이 되게 되고 캐시에 데이터가 올라가게 됩니다.

 

순차적 실행에서는 1번줄이 실행되고 예외가 발생하여 3번줄을 실행하지 않고 넘어갑니다.

; rcx = kernel address, rbx = probe array
xor rax, rax
retry:
mov al, byte [rcx]
shl rax, 0xc
jz retry
mov rbx, qword [rbx + rax]

위와 같은 코드이지만 어셈블리입니다.

 

분기 예측(Branch Prediction)

분기 예측은 파이프라이닝에서 한번 다뤘기 때문에 간단히만 설명하고 넘어가도록 하겠습니다.

 

원래는 분기문이 나올 때마다 분기 조건의 값을 확인하여 다음에 실행할 명령어의 위치(분기 위치, Branch Target)를 계산하고 그곳으로 이동하지만, 분기 예측은 각 분기문 별로 분기할 위치를 미리 예측하여 BTB(Branch Target Buffer)라는 곳에 저장해둡니다. 그리고 분기문을 실행할 때 추측 실행 이 가능한 경우에는 저장되어 있던 예측된 위치를 BTB에서 가져와 실행합니다. 분기 위치의 예측은 직전에 그 분기문이 실행될 때의 분기 위치를 참고해서 그대로 정하는 경우가 많습니다. 정확한 분기 위치는 매번 계산되므로 만약 예측이 잘못된 경우에는 실행결과는 폐기됩니다.

분기 예측으로 인해서 스펙터 취약점이 발생합니다.

다른 유저 프로그램 메모리를 훔쳐보는 취약점입니다.

x86과 ARM 아키텍처에 전부 적용됩니다.

멜트다운보다 덜 치명적이고 구현하기 어렵지만 막기도 어렵습니다.

 

공격

if (x < array1_size)			//#1
	y = array2[array1[x] * 256];	//#2
  1. #1 의 조건이 참이 되도록 x를 설정하여 코드를 여러번 반복시켜 분기예측버퍼(BTB)에 조건문이 참인 위치를 넣습니다. (Branch Target Injection)
  2. array1이 타겟 프로그램의 메모리 영역을 향하도록 x값을 설정합니다 (Bound Check Bypass)
  3. #1의 조건이 거짓이지만 분기예측 에 의해 #2가 실행됩니다.
  4. 3번에서 명령실행 결과 array1의 값이 캐시에 올라갑니다.
  5. Flush+Reload기법을 이용해 타겟 프로그램의 메모리값을 추출합니다.

스펙터 공격의 어려움

  • 기본적으로 공격이 되는 대상 프로그램에서 문제가 존재하는 코드의 추측 실행이 되어야 하기 때문에, 실제 버그를 이용해 공격하려면 공격하려는 대상 프로그램을 세세하게 분석해야 됩니다
  • 프로그램이 돌아가는 CPU 아키텍쳐나 운영체제에 대해서도 자세히 알아야된다. 그래서 실제로는 취약점을 공격하긴 매우 어렵습니다.
728x90

'해킹 > 과제' 카테고리의 다른 글

2. UART 찾기, 칩 구분  (0) 2021.04.20
1. 부트로더, 커널, 파일시스템  (0) 2021.04.20
5. 비순차적 실행(Out-of-order Execution)  (0) 2021.04.17
4. 파이프라이닝(Pipelining)  (0) 2021.04.17
3. CPU의 명령어 처리 구조  (0) 2021.04.16

비순차적 실행은 CPU가 명령어들을 CPU에 넣어지는 순서가 아닌 다른, 더 효율적인 방법으로 실행하는 것입니다.

 

더보기

예를 들어

A = 5

B = 3

C = A + b

D = 4

라는 코드가 있을 때, 순차적 실행을 하게 되면

C = A + B 명령 위에 한 사이클 빈다는 걸 알 수 있습니다. 그래서 비순차적 실행을 적용시켜 다른 코드에 종속성이 없는 코드를 먼저 실행하게 해 성능을 끌어올립니다.

더보기

예를 들어, 값 A는 CPU의 캐시에 있고, 값 B는 없어서 메모리를 읽어와야 하는 상황일 때

var c = B;
var d = A;

위와 같은 코드를 만난다면

순차적 처리의 경우

  1. 값 B를 캐시에서 찾음
  2. 캐시 미스가 발생하고 값 B를 메모리에서 읽어 옴
  3. 값 c를 할당한 뒤, 메모리에서 읽어 온 값 B로 초기화 (이 과정에서 값 A가 캐시에서 사라짐)
  4. 값 A를 캐시에서 찾음
  5. 캐시 미스가 발생하고 값 A를 메모리에서 읽어 옴
  6. 값 d를 할당한 뒤, 메모리에서 읽어 온 값 A로 초기화

비순차적 처리의 경우

  1. 값 B가 필요한 코드를 만났지만 캐시 미스라 일단 대기
  2. 값 A가 필요한 코드를 만났고 캐시 히트
  3. 값 d를 할당한 뒤, 캐시 히트한 A로 초기화
  4. 값 B를 메모리에성 읽어 옴
  5. 값 c를 할당한 뒤, 메모리에서 읽어 온 값 B로 초기화

로 실행하게 됩니다.

 

비순차적 실행에 필요한 개념

명령어 수준 병렬성(Instruction Level Parallelism, ILP): 명령어 수준에서 종속성으로 엮여있지 않아 병렬적으로 작업

1. 명령어 윈도

명령어 스트림 속에서 윈도 크기만큼의 범위 내에서 ILP를 검사하여 비순차적으로 처리합니다. 처리가 끝나면 가장 오래된 명령어가 윈도에서 빠지고 동시에 새로운 명령어 하나가 추가되어 반복합니다.

  • 1. 명령어 하나가 명령어 윈도우 속으로 들어온다. 이와 동시에 제일 오래된 명령어 하나는 완료되어 빠져나간다.
  • 2. 명령어 윈도우 내에서 명령어들을 처리한다. (의존성 체크, 스케줄링 후 병렬 실행, 이전 명령어 완료 기다림, …)
  • 3. 명령어 윈도우 내에서 가장 오래된 명령어가 빠져나가고, 동시에 다른 명령어가 들어온다.

이를 수치화할 수 있는데 만약 4 개의 명령어가 모두 의존성으로 묶여있다면, 4 개 명령어를 4 cycle에 처리할 수 있다고 하여 ILP = 4 / 4 = 1이라고 말합니다. 반면, 2개씩 명령이 의존성으로 묶여있어 4 개의 명령을 2 cycle에 처리할 수 있다면 ILP = 4 / 2 = 2라고 수치화할 수 있습니다. 의존성이 서로 아무것도 없는 명령들이라면 ILP 값은 4 가 되겠네요.

2. 가짜 의존성 제거(Register Renaming)

가짜 의존성이라는 것은 코드에 사용된 레지스터를 보면 의존적인 것처럼 보이지만, 실제로는 의존성이 존재하지 않는 경우를 말합니다.

- Read After Write (RAW): 진짜 의존성

- Write After Write (WAW): 가짜 의존성

- Write After Read (WAR): 가짜 의존성

add r1, r1;    ->    add F1, r1  (가)
add r2, r1;    ->    add F2, F1  (나)
add r1, r2;    ->    add F3, F2  (다)  @ r1 은 (가)와 의존성 없으므로 F3으로 변경 (WAR)
add r1, r3;    ->    add F5, F4  (라)  @ (가)와는 의존성 없으나 (마)와 의존성 존재 (WAW)
add r4, r1;    ->    add F6, F5  (마)  @ (라)와의 의존성 고려하여 F5로 변경 (RAW)

위 코드를 보면, (다) 명령과 (라) 명령은 마치 r1 때문에 (가) 명령과 의존성이 있는 것처럼 보이는데 (다)와 (라)는 r1 레지스터에 쓰기만 할 뿐 읽진 않으므로 가짜 의존성이 됩니다. 그래서 사용하지 않는 다른 레지스터로 변경할 수 있지만, 뒤에 오는 레지스터에서 사용하는지 확인해야 합니다.

이렇게 의존성 체크 과정에서 가짜 의존성이 발견되어 레지스터의 이름을 바꾸어주는 과정을 Rester Renaming이라고 부릅니다.

두 부류의 레지스터 파일을 이용해서 프로그래머가 모르는 채로 내부적인 레지스터의 변경 작업을 하기 위해, 양쪽 레지스터 간의 매핑 관계를 어딘가에 기록해둡니다. 기록을 해두면 이미 사용한 물리 레지스터를 다시 사용하지 않을 수 있고, 선후 명령어 간의 의존성 검사도 잘할 수 있습니다.

논리 레지스터와 물리 레지스터의 매핑 정보를 유지하는 장소를 RAT(Register Alias Table)이라고 부릅니다.

3. 동적 명령어 스케줄링(Reservation Station)

피연산자(operand)가 준비되기를 기다리기 위해 잠시 저장해두는 곳이 Reservation Station이라는 Queue입니다.

 

  • Operand 준비됐으니 실행 -> Wake-up
  • 이제 실행하면 되니까 일 안 하는 실행 장치 찾기 -> Select

Wake-up 작업은 어떤 선행 연산이 끝나서 피연산자가 준비가 됐는데, Reservation Station에서 기다리는 어떤 명령어가 이 피연산자를 기다리고 있었나 찾는 것을 말합니다. 이것을 찾으려면 매번 Reservation Station Queue에 있는 명령어들이 필요로 하는 피연산자들을 모두 체크하게 됩니다. (병목 지점)

Select 작업은 위에 말한 대로 피연산자가 준비되어 실행 가능한 상태의 명령어보다 사용 가능한 실행 장치의 숫자가 적은 상황이 있을 수 있으므로 이러한 실행 장치의 할당을 효과적으로 수행할 수 있도록 스케줄링하는 것을 말합니다. (병목지점)

이러한 병목 현상과 복잡성 때문에 Reservation Station Queue의 크기를 무작정 늘려서 병렬성을 높이는 작업에 한계가 있습니다.

결국 Reservation Station 이 존재함으로 해서 명령어들의 실행이 반드시 순차적이지 않고, 피연산자가 준비된대로 비순차적으로 스케줄링되어 실행이 가능한 것입니다.

4. 순차적 완료(Re-order Buffer)

위에서 봤듯이 명령어의 실행은 비순차적일 수 있고 결과도 비순차적으로 나올 수 있으며, 명령어 병렬화로 순서가 섞여서 끝났어도 결과적으론 원하는 순서대로 결과를 보여주는 과정이 필요하고, 비순차적인 결과를 순차적으로 만드는데 Re-order Buffer 가 사용됩니다.

Re-order Buffer 에 결과들을 모아뒀다가 원래의 프로그램 순서대로(In-order) 데이터 Commit 을 해줍니다.

그렇게 하기 위해서 Re-order Buffer 에 끝난 순서대로 넣는것이 아닌, Reservation Station에서 Issue 되는 시점에서 Entry를 할당받아 원래 순서대로 넣습니다.

실행이 다 완료되면 끝난것들만 표시해두고, 처리가 종료 되면 표시가 된 것들 중에서 Reorder Buffer 에서 차례대로 그 결과를 레지스터 등에 Commit 해줍니다.

 

728x90

파이프라인은 일반적으로 IF(Instruction Fetch), D(Decode), E(Execute), MEM(Memory), WB(WriteBack) 단계를 가집니다.

기존 방식
파이프라이닝 적용

파이프 라이닝은 여러 명령어를 중첩하여 실행시키는 기술입니다.

한 명령어를 처리하고 그 이후에나 다음 명령어를 처리시키는 기존 방식과 달리 한 사이클 안에서 쉬는 컴포넌트 없이 작업하여 더 효율적인 처리를 가능토록 합니다.

위에 사진에서 보면 알 수 있듯이 15사이클 만에 끝나던 것이 8사이클 더 빨리 7사이클 만에 끝났습니다.

모든 단계가 동일한 시간에 끝나면 좋겠지만 실제로는 처리시간이 다르기 때문에 약간의 중간중간 쉬는 시간이 있습니다.

Reg에 100ps 간격의 IDLE 발생

x86(CISC)의 경우 명령어 중에 메모리에 직접 접근하는 명령이 있기 때문에 파이프라인에 바로 적용하면 명령어가 다른 명령어들에 비해 늦게 끝나 파이프라인을 뒤흔들 수 있고, x86의 명령어는 복잡하기 때문에 데이터 종속성으로 인해 중단될 가능성이 높습니다.

그래서 x86 명령어는 디코딩된 다음 μops(micro-operations)로 변환하여 μops로 변환하여 1사이클에 1 코드 실행이 가능하게 합니다.

 

 

width: 사용 가능한 파이프라인의 수 (최신 인텔 프로세서에선 일반적으로 4 개입니다.)

slots: 주기 당 프런트 엔드에서 발행하고 백엔드에서 폐기할 수 있는 고정된 수의 μop

(하나의 uop를 처리하는 데 필요한 하드웨어 리소스)

software.intel.com/content/www/us/en/develop/documentation/vtune-help/top/reference/cpu-metrics-reference/pipeline-slots.html

 

front-end: 프런트 엔드는 명령어를 가져오고 해석하여 μop로 변환합니다.

front-end-bound: 백 엔드가 이를 수용할 수 있음에도 불구하고 명령이 프런트 엔드에서 백 엔드로 이동하지 못할 때 발생합니다.

back-end: 비 순차적 스케줄러가 준비된 μop들을 각각의 실행 단위로 디스 패치하고, 완료되면 이러한 μops는 프로그램 순서에 따라 폐기되는 프로세서 코어의 일부

back-end-bound: 백엔드에서 새 μop들을 수락하는 데 필요한 리소스가 부족하여 μop들이 제공되지 않는 파이프 라인 슬롯 비율(프런트 엔드와 백엔드 모두 stall시 백엔드로 구분)

allocation: front-end에서 μop를 back-end로 넘기는 과정

retirement: μop의 실행 완료(보통 대부분의 μop들은 완전히 파이프라인을 통해 통과하고 리타이어 되지만, 종종 예측적으로 패치된 μop들은 리타이어 전에 취소될 수도 있습니다. 예: 예측 실패한 분기의 경우)

 

software.intel.com/content/www/us/en/develop/documentation/vtune-help/top/reference/cpu-metrics-reference/back-end-bound.html

2-width 파이프라인

각 슬롯은 포함된 uop에 어떤 일이 발생하는지에 따라 주어진 주기에서 네 가지 범주 중 하나로 분류할 수 있습니다.

파이프라인 slot 분류 순서도

Front-End-Bound

프런트 엔드 바운드 슬롯의 예시

이 예에서 명령어 B는 디코딩이 끝날 때까지 추가 주기를 취하고, 백엔드로 전달하는 대신 사이클 4의 해당 단계에 남아 있습니다. 이것은 여기에 느낌표로 표시된 파이프 라인 버블이라고 알려진 파이프 라인 아래로 전파되는 빈 공간을 만듭니다.

Back-End-Bound

백엔드 바운드 슬롯의 예시

이 예에서 명령 B는 실행하는 데 추가 사이클이 필요하며, 여전히 사이클 5에서 Execute 단계를 차지하고 있기 때문에 명령 C는 백엔드로 이동할 수 없습니다. 이것은 또한 파이프 라인 버블을 초래합니다.

 

꼭 Decode 또는 Execute 단계에서 지연이 발생하지 않아도 됩니다.

 

프런트 엔드 바운드 그림에서 B가 가져오기 위해 추가 주기를 수행한 경우 사이클 3에서 Decode 단계로 전달된 명령이 없어서 버블이 생성됩니다. 따라서 사이클 4에서 백엔드로 전달하라는 명령이 없습니다.

 

백엔드 바운드 그림에서 명령 A가 메모리 단계에서 추가 주기를 수행한 경우, 그러면 B는 준비가 되었는지 여부에 관계없이 사이클 5에서 Execute 단계를 벗어날 수 없었을 것입니다. 따라서 C가 백엔드로 진행하는 것을 차단하여 원래 위치에 남아 있습니다.

Bad Speculation

부분적으로 처리된 μop이 완료되기 전에 취소될 때마다 발생합니다. μop이 취소되는 가장 일반적인 이유는 분기 예측 오류 때문입니다.

분기가 해결되기 전에 파이프 라인으로 로드되는 경로 X의 명령

정교한 알고리즘을 사용하여 분기 명령이 Execute 단계의 끝에 도달하기 전에 경로 X를 사용할지 Y를 사용할지 추측하여 파이프라인에 로드합니다.

여기에서 두 가지 가능한 결과가 있습니다.

  1. 분기 예측이 정확하고 상황이 정상적으로 진행됩니다.
  2. 분기가 잘못 예측되어 잘못된 명령이 삭제되고 거품이 제자리에 남고 올바른 명령이 파이프 라인에 들어가기 시작합니다.

성능 저하는 파이프 라인이 명령 로드를 시작하기 전에 분기 실행이 해결될 때까지 기다린 것과 사실상 동일하지만 분기가 발생할 때마다 발생하는 것이 아니라 예측 알고리즘이 잘못된 경우에만 발생합니다. 이 때문에 예측 알고리즘을 개선하기 위한 지속적인 노력이 있습니다.

 

결론적으론 분기 예측을 해서 맞으면 성능이 좋아지고, 예측에 실패하여 버블이 생긴다 하더라도 분기가 Execute 단계의 끝에 도달할 때까지 기다리는 성능과 같기 때문에, 성능 저하가 없습니다.

올바른 분기 예측
분기 예측 오류

Pipeline Hazards

- 구조적 해저드(Structural Hazard)

한정된 자원. 한 개 이상의 명령어를 중복된 하드웨어에서 처리하고자 하는 상황이 발생합니다.

내부 버스에 두 모듈이 하나의 모듈로 동시에 신호를 보내면 처리가 불가능해서 구조적 Hazard 가 생깁니다.

예로는 메모리 입출력이 있습니다.

자원을 더 추가하거나 파이프라인 버블을 만들어 해결할 수 있습니다. (Pipeline Freezing이라고 부릅니다.)

동시에 메모리 접근 요구

- 데이터 해저드(Data Hazard)

명령어가 아직 처리 중인 앞선 명령어에 종속성을 가질 때 발생합니다.

- Read After Write (RAW)

- Write After Write (WAW)

- Write After Read (WAR)

RAW 예시

예시에서는 첫 번째 명령에서 $2에 저장된 결과를 두 번째, 세 번째 명령에서 활용합니다.

pipeline을 적용하면서 $2가 저장되기 이전에 그 이후에 수행되어야 할 명령이 미리 수행되어 버려서

메모리에 값을 쓰기 전에 다른 명령에서 읽게 됩니다. 그러면 and와 or 연산은 $1과 $3이 Sub 한 결과가 아닌 이전에 들어있던 쓰레기 값으로 연산을 하게 됩니다.

해결방법은 간단합니다. pipeline freezing을 하여 앞선 명령어가 다 끝날 때까지 대기하거나, forwarding을 하여서 명령 실행 중간에 다른 모듈로 값을 넘겨주면 됩니다.

pipeline forwarding

- 제어 해저드(Control Hazard)

분기 명령어의 결과를 MEM 단계에서 알 수 있기 때문에 결과가 나오기 전 명령이 잘못 처리되는 일이 발생합니다.

분기 예측(Branch prediction)을 사용하여 해결할 수 있습니다.

728x90

위 자료들은 CPU가 명령을 처리할 때 명령을 주기억 장치에서 가져오고, 해독, 실행하는 절차입니다.

 

명령 주기

1. Instruction Fetch(명령 인출)

cpu가 전 글에서 설명한 명령어를 메모리에서 가져오는 과정입니다.

 

2. Instruction Decode(명령 해석)

가져온 명령어의 opcode를 해석하고, 적절한 레지스터로 이동합니다.

 

3. Read the effective address(유효 주소 읽기)

명령어에 간접 주소 가있는 경우 유효 주소를 주 메모리에서 읽고 필요한 데이터를 주 메모리에서 가져와서 처리 한 다음 데이터 레지스터에 배치합니다.

I/O 명령어 또는 레지스터 명령어인 경우 현재 클럭동안 수행됩니다.

4. Instruction Execute(명령 실행)

CPU의 제어 장치는 디코딩 된 정보를 일련의 제어 신호로 CPU의 관련 기능 장치에 전달하여 레지스터에서 값을 읽고이를 수행 할 ALU(Arithmetic Logic Unit)로 전달하는 등 명령에 필요한 작업을 수행합니다. 수학적 또는 논리 함수를 사용하고 결과를 레지스터에 다시 기록합니다. ALU가 관련되어 있으면 조건 신호를 CU(Control Unit)로 다시 보냅니다. 연산에 의해 생성 된 결과는 메인 메모리에 저장되거나 출력 장치로 전송됩니다. ALU의 피드백에 따라 PC(Program Counter, 레지스터)는 다음 명령을 가져올 다른 주소로 업데이트 될 수 있습니다.

5. 반복

728x90

명령어 포맷에 있는 opcode 가 명령어를 구분해주기 때문에 중요하게 쓰입니다.

CISC

CISC는 명령어의 길이가 가변적이기 때문에 Prefixes, Displacement, Immediate들이 0~4바이트까지, Opcode 가 1~3바이트까지 가변적으로 변합니다.

RISC

opcode: 어떤 명령어고 어떻게 작동해야 하는지 수식

operand: 피연산자

Immediate: 상수 또는 인코딩된 숫자 표현식

funct7, funct3: opcode와 함께 쓰이고 수행할때 어떻게 작동할지 설명

rs1, rs2: 첫번째, 두번째 피연산자 레지스터

rd: 결과를 저장할 레지스터

 

RISC는 모든 명령어의 길이가 다 같아서 이렇게 포맷이 6가지로 정해져 있습니다.

 

Register/register: 논리/산술 연산

Immediate: 상수 연산(메모리에서 상수를 가져오는 lw연산을 피할 수 있으므로 연산을 매우 빠르게 할 수 있음), 메모리 로드

Upper Immediate: 32bit의 immediate value를 특정 register에 넣을 수 있음.

Store: 메모리에 저장

Branch: 레지스터 비교

Jump: 해당 위치로 점프

 

riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf

CPU 구성요소

1. 프로세서 레지스터

 

0. 레지스터, 캐시, 램, 클럭 설명

램 램은 메인보드에 램 슬롯이란 곳에 끼워서 사용하며 cpu와는 버스라는 시스템을 이용해서 통신하게 됩니다. 이 버스는 일반적으로 주소 버스, 제어 버스, 데이터 버스의 세 부분으로 구성되게

rmagur1203.tistory.com

2. 산술 논리 연산 장치(Arithmetic Logic Unit)

두 숫자의 산술 연산과 논리 연산을 계산하는 디지털 회로입니다.

- 산술 연산 장치 : 산술 연산(+ - * /) 수행

- 논리 연산장치 : 논리 연산 (AND, OR, XOR, NOT...)을 수행

- 보수기(complementer) : 데이터에 대하여 2의 보수를 계산 (음수화)

- 시프트 레지스터(shift register) : 비트들을 좌, 우로 이동시키는 기능을 가진 레지스터

- 상태 레지스터(status register) : 연산 결과의 상태를 나타내는 플래그(flag)를 저장하는 레지스터

2-1. 부동소수점 장치(Floating Point Unit)

ALU와 비슷하지만 부동소수점 실수에 대한 사칙연산을 지원하며, 모듈에 따라 거듭제곱 뿐만 아니라 삼각 함수 등 다양한 수학 계산을 할 수 있습니다.

3. 제어부

 

3. CPU의 명령어 처리 구조

위 자료들은 CPU가 명령을 처리할 때 명령을 주기억 장치에서 가져오고, 해독, 실행하는 절차입니다. 명령 주기 1. Instruction Fetch(명령 인출) cpu가 전 글에서 설명한 명령어를 메모리에서 가져오는

rmagur1203.tistory.com

4. 내부 버스

중앙처리장치 내 각 구성 요소를 연결하여

데이터, 주소 또는 제어신호를 전달하는 신호 회선입니다.

 

연산장치와 레지스터들 사이의 데이터 통로가 되며,

데이터 선과 제어장치로부터 발생하는 제어 신호선으로 구성됩니다.

 

내부 버스는 시스템 버스와 직접 연결되지 않으며,

버퍼 레지스터를 통해 시스템 버스와 연결됩니다.

728x90

'해킹 > 과제' 카테고리의 다른 글

4. 파이프라이닝(Pipelining)  (0) 2021.04.17
3. CPU의 명령어 처리 구조  (0) 2021.04.16
1. CPU 명령어 집합 구조  (0) 2021.04.16
0. 레지스터, 캐시, 램, 클럭 설명  (0) 2021.04.16
2. 메모리들의 작동 방식 - RAM  (0) 2021.04.09

CPU 명령어 집합 구조(Instruction Set Architecture)는 크게 두 가지로 나뉩니다. CISC와 RISC.

 

CISC(Complex Instruction Set Computer)

직역하자면 복잡한 명령어 집합 컴퓨터 입니다.

 

장점

컴파일러 작성이 간단합니다.

명령어들의 길이가 각자 다 다르기 때문에 CISC에서 2비트짜리 명령어도 RISC에서는 고정된 크기의 명령어가 되기 때문에 전체적인 프로그램의 용량이 줄어들게 됩니다.

 

복잡한 명령어를 쓸 때에는 CISC 가 RISC 에 비해서 코드를 하나만 입력해도 작동하기 때문에 직접 어셈블리어를 수정하는 사람의 입장에서는 생산성이 높으며, RISC는 여러 코드를 입력해야지 되는 것에 비해서 CISC는 한 코드로 수행이 가능하기 때문에 명령어의 길이가 줄어들며, 메모리에 접근하는 횟수도 줄어들게 됩니다.

 

RISC는 CPU 구조가 바뀌어 처리 비트 단위가 바뀌게 되면 기존의 명령어를 사용할 수 없지만, CISC 는 명령어의 길이가 다 달라서 CPU의 구조가 바뀐다 해도 예전 명령어들을 사용할 수 있습니다.

 

예전에 메모리 가격이 매우 비쌌던 시절에는 CISC가 메모리를 적게 쓰기 때문에 CISC를 먼저 쓰게 되었습니다.

단점

명령어가 복잡한 것이 많고 길이가 가변적이기 때문에 프로세서의 설계가 힘듭니다.

 

일정한 길이로 명령어가 나뉜 RISC와 다르게 CISC는 명령어의 길이가 가변적이며, 메모리를 소스 혹은 타겟으로 이용할 수 있어서 사이클 계산이 어렵고, 다중 사이클 명령어가 많아 파이프라이닝 구현이 매우 어렵습니다. CISC는 몇 클럭의 마이크로코드가 입력될지 알 수 없기 때문에 이를 적절하게 쪼개 파이프라인으로 전달해줘야 해, 파이프라이닝 구현이 RISC에 비해 난해해집니다. 그래도 지금은 인텔이 슈퍼스칼라를 고치면서 해결방법을 찾아서 파이프라이닝 구현 난이도가 많이 낮아졌습니다. 하지만 그래도 RISC 가 CISC에 비해서 파이프라이닝 효율도 좋고 확장도 편합니다.

 

해독이 어려워 디코더 설계가 까다로우며 디코더가 굉장히 커집니다.

 

CISC는 코드 길이를 가변적으로 입력할 수 있기에, 프로그래머의 실력이 떨어지면 코드를 필요 이상으로 길게 작성하는 일이 발생해 큰 퍼포먼스 저하를 유발할 수 있습니다.

 

기술이 발전해가면서 장점들이 의미가 없어지게 되었습니다.

 

더보기
  • 명령어 길이가 제각각이다. 따라서 명령어를 인출할 때 언제가 명령어 종료 시점인지 알 수 없으며 다중 사이클이 소요되는 명령어를 그대로 파이프라인 처리를 할 경우 파이프라인 버블이 발생하여 처리효율을 떨어뜨린다.
  • 명령어 포맷이 중구난방이다. 때문에 명령어 해석기에서 특정 내용을 액세스하려고 할 경우 그 명령어에 대한 인출과 해석이 완전이 끝나야 가능하다.
  • 명령어에 레지스터 대상이 아닌 메모리를 대상으로 한 연산이 섞여있다. 메모리를 대상으로 한 연산의 경우 메모리 액세스 사이클의 손실이 정확히 얼마일지 알 수 없기 때문에 파이프라인의 정확한 운용이 어려워진다.
  • 각 명령어들의 실행 사이클이 제각각이다. 어떤 명령어는 1사이클, 어떤 명령어는 3사이클 하는 식으로... 이러한 구조는 파이프라인의 효율성을 심각하게 저하시킨다.
  • 마이크로코드를 통해 구현된 복잡한 기능의 명령어는 처음 도입할 때는 타당성이 있었을지 모르지만 CPU 기술이 발전하면서 다른 방식으로 처리하는 게 더 빨라지는 사태가 발생했다.
  • 마이크로코드 방식 자체가 CPU의 명령어 해석기 고속화에 발목을 잡게 되었다.
  • 명령어의 구조가 어큐뮬레이터 구조를 유지하고 있다. 명령어가 2개의 레지스터/메모리만을 액세스 가능하므로 a+b의 연산을 c에 따로 저장하지 못하고 b의 내용에 덮어쓰면서 b의 내용을 재활용하지 못하게 된다.
  • 레지스터의 기능이 일정하지 못하고 특수기능이 포함된 경우가 많아 컴파일러의 복잡성을 증대시켰다.
  • 프로그램 코드와 데이터가 단일 캐시메모리 공간에 위치하고 있는 폰노이만 구조는 코드의 메모리 동작 특성과 데이터의 메모리 동작 특성이 크게 다르다는 사실을 반영하지 못하고 있다.
  • ROM 기반이던 마이크로코드 해석기는 수정이 불가능하고 유연성이 떨어지지만 속도가 상대적으로 빠르다는 장점이 있었는데 RAM 기술이 비약적으로 발달하면서 이런 장점도 퇴색되었다.

RISC(Reduced Instruction Set Computer)

직역하자면 줄어든 명령어 집합 컴퓨터입니다.

CISC보다 명령어가 줄어들었고, 명령어의 길이가 다 같아졌습니다.

 

장점

컴퓨터는 수행해야할 각 명령어 형태마다 트랜지스터와 회로들이 추가되어야 하기 때문에, 많은 수의 컴퓨터 명령어 셋을 갖춘 마이크로프로세서는 만들기도 복잡하고, 실행속도도 늦습니다. RISC는 명령어 셋이 CISC보다 적습니다.

 

메모리를 직접 소스, 타겟으로 이용할 수 없어서 사이클 계산이 쉽습니다.

 

명령어셋이 적기 때문에 하드웨어가 간단해지고 아키텍쳐를 설계하는데 어렵지 않습니다.

 

전력 소모가 적으며 속도가 빠르고 가격이 저렴합니다.

 

모든 명령어의 길이가 똑같고 명령어는 한 개당 하나의 동작만을 수행하게 되므로 메모리에서 데이터를 레지스터에 가져온 이후에 다른 명령어가 메모리 접근하지 않기 때문에 파이프라인이 끊기지 않고 수행될 수 있습니다. 그래서 파이프라인 구현이 쉽습니다.

 

단점

단순화를 위해 코드 밀도가 감소하여 같은 내용을 처리하는 데 더 많은 코드 용량이 필요하게 되었습니다.

- 항상 16bit 혹은 32bit를 차지하는 고정 길이 명령어는 상황에 따라 8~32bit를 오가는 CISC의 가변 길이 명령어에 비해 코드 밀도 면에서 불리합니다.
- 메모리를 대상으로 하는 연산 명령어의 경우 CISC에서는 1개 명령어로 표현 가능하지만 반면 RISC에서는 load-execute-store로 3개의 명령어가 필요합니다.
- 마이크로코드로 한 줄로 구현된 CISC 명령어를 몇 개, 혹은 수십 개의 RISC 명령어로 변환해야 합니다.
- 평균적으로 같은 역할을 수행하는 RISC의 코드 길이는 x86에 비교하면 2배나 길다.

 

메모리를 소스 혹은 타깃으로 쓸 수 없게 되면서 그 역할을 대체해야 하는 더 많은 레지스터가 필요하게 되었습니다.

 

명령어 길이가 제약되면서 분기 명령의 점프 범위가 제약되었다. CISC에서는 분기 명령의 점프 범위를 단순히 코드 길이를 늘리는 것으로 해결할 수 있지만 RISC 명령어는 불가능해졌습니다.

 

x86이 아니였기 때문에 Windows 를 돌릴 수 없다는게 가장 큰 단점이였습니다.

728x90

 

램은 메인보드에 램 슬롯이란 곳에 끼워서 사용하며 cpu와는 버스라는 시스템을 이용해서 통신하게 됩니다.

이 버스는 일반적으로 주소 버스, 제어 버스, 데이터 버스의 세 부분으로 구성되게 됩니다.

 

메모리에서 데이터를 읽어 들일 때 CPU는 주소 버스를 통해 메모리 위치를 지정하고, 주소 디코더에 의해 해당 메모리는 데이터 버스에 데이터를 보냅니다. 그리고 CPU는 데이터 버스의 데이터를 읽게 됩니다. 메모리에 데이터를 쓸 때에 CPU는 메모리의 위치를 주소 버스로 지정하고, 데이터 버스에 데이터를 보냅니다. 메모리는 데이터 버스의 데이터를 받아 저장하게 됩니다.

 

하지만 CPU와 메모리, IO장치는 데이터 버스를 통해 데이터를 통신하기 때문에, 장치가 여러 개 있으면 데이터 버스도 여러 개 필요하기 때문에 메모리와 IO장치는 버스를 공유하게 되고, 이 때문에 cpu는 동시에 한 개의 장치하고만 통신할 수 있습니다. 그래서 한 개의 데이터 버스로 여러 장치를 이용할 수 있게 원하는 장치랑 통신할 수 있도록 회로를 구성해 주는데 이것을 주소 디코딩이라 하며, 이 일을 수행하는 장치를 주소 디코더라 부릅니다.

 

캐시

CPU 칩 안이나 그 근처에 두는 SRAM으로 이루어진 매우 빠른 메모리입니다.

DRAM도 CPU의 동작속도에 비하면 느리기 때문에 병목현상을 또 줄이고자 만들어진 작은 메모리입니다.

프로그램에서 직접적으로 읽거나 쓸 수 없고 하드웨어의 메모리 관리 시스템(MMU)이 내부적으로 제어하게 됩니다.

보통 L1, L2, L3의 캐시가 있으며 L3로 갈수록 크기가 커지며 L1으로 갈수록 크기가 작아집니다.

캐시의 용량이 크면 클수록 더 많은 데이터를 캐시에 캐싱해둘 수 있기 때문에 램에서 불러오는 횟수도 줄어들고 cpu의 처리 속도도 빨라져서 좋지만 비싸기 때문에 적당히 달게 됩니다.

그리고 데이터를 처음 접근할 때마다 불러오면 속도가 느려지게 되기 때문에, 시간적 지역성, 공간적 지역성을 이용하여 데이터를 미리 캐싱해두게 됩니다.

시간적 지역성

예전에 한번 접근했던 데이터는 나중에 다시 접근할 가능성이 높다.

공간적 지역성

가까운 주소에 연속적으로 접근된 데이터가 있으면 현재 데이터도 접근할 가능성이 높다.

한 메모리 주소에 접근할 때 그 주소뿐 아니라 해당 블록을 전부 캐시에 가져오게 된다.

 

레지스터

레지스터는 기본적으로 캐시와 같은 구조의 플립플롭 회로를 이용하여 저장하지만, CPU 바로 옆에 붙어있기 때문에 응답속도가 캐시에 비해서 빠릅니다.

32/64 비트 시스템이라 부르는 것이 하나의 레지스터에 저장할 수 있는 용량이 얼마인지 나타내는 것입니다.

32비트의 경우 한 번에 표현할 수 있는 메모리의 주소가 4GB 이기 때문에 인식할 수 있는 메모리의 크기도 4GB입니다.

64비트 레지스터
32비트 레지스터

앞에 붙어있는 E와 R은 E는 32비트, R은 64비트를 나타냅니다.

그리고 EAX, AX, AH, AL 모두 다른 레지스터가 아니라

32비트 레지스터 EAX 중 16비트가 AX, AX의 상위 8비트가 AH, 하위 8비트가 AL입니다.

즉 EAX레지스터가 AX를 포함하고, 이 AX가 AL, AH를 포함하고 있는 형태입니다.

 

레지스터의 종류는 여러 가지가 있습니다.

  • 데이터 레지스터 : 정수 값을 저장할 수 있는 레지스터.
  • 주소 레지스터 : 메모리 주소를 저장하여 메모리 접근에 사용되는 레지스터. 어떤 프로세서에서는, 주소를 저장하는 것이 아니라 조작하기 위한 목적으로 색인 레지스터를 사용하기도 한다.
  • 범용 레지스터 : 데이터와 주소를 모두 저장할 수 있는 레지스터.
  • 부동소수점 레지스터 : 많은 시스템에서 부동소수점 값을 저장하기 위해 사용된다.
  • 상수 레지스터 : 0이나 1 등 고정된 값을 저장하고 있는 레지스터.
  • 특수 레지스터 : 프로그램의 상태를 저장한다. PC, SP, SR 등이 있다.
    • 명령 레지스터 : 현재 실행 중인 명령어를 저장한다.
    • 색인 레지스터 : 실행 중에 피연산자의 주소를 계산하는 데 사용된다.

 

CPU에는 여러 가지 종류의 레지스터가 들어있습니다.

 

범용 레지스터

AC (Accumulator) : 연산 결과 데이터를 일시적으로 저장하는 레지스터

 

명령 레지스터

PC (Program Counter) : 다음 인출(Fetch) 될 명령어의 주소를 가지고 있는 레지스터

IR (Instruction Register) : 가장 최근에 인출된 명령어(현재 실행 중인 명령어)가 저장되어 있는 레지스터

 

데이터 레지스터(데이터 버스에 데이터를 읽고 쓰기 위해 잠시 저장해두는 버퍼 레지스터)

MBR (Memory Buffer Register) : 기억장치에 저장될 데이터 혹은 읽힌 데이터가 일시적으로 저장되는 버퍼 레지스터

 

주소 레지스터(주소 버스에 주소를 출력하기 전에 임시 저장하는 레지스터)

MAR (Memory Address Register) : PC에 저장된 명령어 주소가 사용되기 전에 일시적으로 저장되는 주소 레지스터

SP (Stack Pointer) : 스택의 데이터가 채워진 마지막 위치를 가리킵니다.

BP (Base Pointer) : 스택의 한 기준점을 가리킵니다.

IX (Index Pointer) : 배열을 순차적으로 액세스 하기 위한 레지스터입니다.

 

특수 레지스터

SR (Status Register) : 현재 CPU의 상태를 가지고 있는 레지스터

 

 

 

클럭

연산장치 또는 외부에 클럭 제네레이터가 클럭 펄스라는 것을 만들어 내부장치를 동기화시키는 데 사용합니다.

클럭에 따라서 명령어가 실행됩니다.

728x90

'해킹 > 과제' 카테고리의 다른 글

2. 명령어 구조, CPU 구성요소  (0) 2021.04.16
1. CPU 명령어 집합 구조  (0) 2021.04.16
2. 메모리들의 작동 방식 - RAM  (0) 2021.04.09
1. 메모리들의 작동 방식 - ROM  (0) 2021.04.06
2-1. 컴퓨터 부팅 과정 - UEFI  (0) 2021.04.06

RAM(Random Access Memory)

자유롭게 임의의 위치에 접근하여 내용을 읽고 쓸 수 있고, 어느 위치에나 똑같은 속도로 접근하고 쓸 수 있는 메모리로 주로 휘발성을 갖습니다.

ROM에 비해 읽고 쓰기가 자유롭고 속도도 훨씬 빠르기 때문에 보조기억장치와 중앙처리장치 사이의 병목을 줄여주는 역할로 주로 사용합니다.

1. SRAM(Static RAM)

SRAM은 내용을 한번 기록하면 전원이 공급되는 동안 내용을 그대로 갖고 있습니다. 그리고 DRAM의 100배 이상으로 접근 속도가 빠르지만 구조가 복잡하여 공간을 많이 차지하므로 집적도를 높이기 어려워 가격이 비싸고 대용량으로 제작하기가 어렵습니다. 속도를 대략 측정해보면 초당 수 백 GB/s를 넘는 정도로 매우 빠릅니다. 그래서 빠른 속도가 요구되는 캐시 메모리(Cache Memory)에 주로 사용됩니다.

2. DRAM(Dynamic RAM)

DRAM은 내용을 기록한 뒤에 그 내용을 유지하기 위해서 주기적으로 다시 써줘야 합니다. 캐패시터로 이루어져 있고, 캐패시터의 충전 상태로 내용을 기록하기 때문에 캐패시터가 방전이 될 때마다 다시 써줘야지 되는 것입니다.

DRAM은 속도가 SRAM보다는 느리지만 구조가 간단하여 집적도를 쉽게 높일 수 있습니다. SRAM에 비해 속도는 느리지만 가격이 싸고 집적도를 쉽게 높일 수 있기 때문에 캐시보다는 느린 주 기억 장치에 주로 쓰이게 됩니다.

보통 우리가 램이라고 부르는 것들은 대부분 DDR SRAM(Double Data Rate Synchronous Dynamic RAM) 혹은 LPDDR SDRAM(Low Power DDR SDRAM)을 지칭합니다.

728x90

'해킹 > 과제' 카테고리의 다른 글

1. CPU 명령어 집합 구조  (0) 2021.04.16
0. 레지스터, 캐시, 램, 클럭 설명  (0) 2021.04.16
1. 메모리들의 작동 방식 - ROM  (0) 2021.04.06
2-1. 컴퓨터 부팅 과정 - UEFI  (0) 2021.04.06
2. 컴퓨터 부팅 과정 - BIOS  (1) 2021.04.06

+ Recent posts