용어 정리

변수: 변하는 값을 저장하는 곳. 데이터를 저장하기 위해 메모리에서 공간을 할당 받음.

주소값: 변수에서 메모리에서 할당받은 공간의 시작 주소

포인터: 위의 주소값을 저장하는 변수(또는 상수)

상수 포인터: 다른 주소값을 가리키도록 바꿀 순 있으나, 그 주소에 있는 값을 바꿀 순 없음.

포인터 상수: 다른 주소값을 가리키도록 바꿀 수 없으나, 그 주소에 있는 값을 바꿀 순 있음.

 

포인터 선언

변수타입* 변수명;
변수타입 * 변수명;
변수타입 *변수명 = 주소값;

으로 선언할 수 있습니다. (*이 어디 붙어도 상관없습니다.)

int a = 10
int* b = &a;

연산자

& 연산자(주소 연산자)

&변수

이렇게 사용할 수 있다.

int a = 10;
print("%p", &a); // %p 는 주소값을 출력할 때 사용합니다.

이런 식으로 & 연산자를 사용하게 되면 변수 a의 주소값이 출력됩니다.

* 연산자(참조 또는 역참조 연산자)

* 연산자는 뒤에 오는 주소값에서 그 주소에 담겨있는 값에 접근하는 데 사용합니다.

*변수

예를 들어 "*&변수"를 하게 되면 변수의 주소값에서 그 주소에 담겨있는 값을 가져오게 되므로 변수의 값이 나오게 됩니다.

int a = 948;
int* b = &a;
printf("%p %d\n", &a, *&a);
printf("%p %d", b, *b);

결과는

(a의 주소값) 948

(a의 주소값) 948

이 나오게 됩니다.

포인터 연산

포인터의 덧셈과 뺄셈

포인터의 덧셈

int a = 50;
int *b = &a;

char c = 'c';
char* d = &c;

printf("%p\n", b); //0061FF14
printf("%p\n", d); //0061FF13

printf("%p\n", b + 1); //0061FF18
printf("%p", d + 1); //0061FF14

* 포인터 간의 덧셈은 안됩니다. (쓸 이유가 없음)

* 포인터에 x만큼 더하면 주소값은 (x * 자료형) 만큼 늘어납니다.

포인터의 뺄셈

int a[2] = {50, 100};

int *a1 = a;
int *a2 = a + 1;

printf("%d\n", *a);
printf("%d\n", *(a + 1));

printf("%p %d\n", a1, *a1);
printf("%p %d\n", a2, *a2);
printf("%d\n", a2 - a1);

* 배열 자체는 배열의 첫 번째 주소를 나타내므로 *a는 배열의 첫 번째 항목, *(a+1)은 배열의 두 번째 항목이 됩니다.

* a1은 배열의 첫 번째 항목의 주소, a2는 배열의 두 번째 항목의 주소이므로 a2-a1은 배열의 두 번째 항목과 첫번째 항목의 주소의 차가 됩니다.

* a2 - a도 똑같이 첫번째 항목과 두번째 항목의 주소의 차가 되고 이걸 이용하면 a2가 배열에서 위치한 위치를 알 수 있습니다.

* lowerbound나 upperbound를 사용할 때 반환값에서 배열을 빼면 검색한 항목의 위치(index)를 알 수 있습니다.

포인터의 곱셈과 나눗셈

불가능합니다.

Call By Value

#include <stdio.h>

void test(int a){
    a = 10;
    printf("in test function, variable a value: %d\n", a); //10
}

int main()
{
    int a = 90;
    printf("before invoke test function, variable a value : %d\n", a); //90
    test(a);
    printf("in main function, variable a value: %d", a); //90
    return 0;
}

함수의 인자에 변수의 값을 복사해서 전달해준다.

함수 내에서 값을 바꿔도 원래 변수의 값이 변하지는 않는다. 

Call By Reference

#include <stdio.h>

void test(int *a)
{
    *a = 10;
    printf("in test function, variable a value: %d\n", *a); //10
}

int main()
{
    int a = 90;
    printf("before invoke test function, variable a value : %d\n", a); //90
    test(&a);
    printf("in main function, variable a value: %d", a); //10
    return 0;
}

함수의 인자에 변수의 주소값을 전달해준다.

함수에서 참조 연산자를 이용해서 값을 바꾸면 원래 변수의 값도 변한다.

다중 포인터

포인터도 변수이므로 포인터에 대한 주소값을 저장하는 포인터를 만들 수 있습니다.

이것을 다중 포인터라 부르게 됩니다. 아래 예시를 보면

int a = 10;
int *b = &a;
int **c = &b;
printf("%p %p %p\n", &a, b, *c);
printf("%d %d %d", a, *b, **c);

포인터 c는 포인터 b의 주소값을 저장하는 2중 포인터인 것을 알 수 있습니다.

포인터를 선언할 때 변수타입* 변수명; 으로 한다고 했었는데 int **c = &b;에서 (int*)가 자료형이 됩니다.

자료형*변수명 = 주소값;

int *   *c        = &b;

 

3중 포인터 예시를 보면

int a = 10;
int *b = &a;
int **c = &b;
int ***d = &c;
printf("%p %p %p %p\n", &a, b, *c, **d);
printf("%d %d %d %d", a, *b, **c, ***d);

이런 식으로 한 겹이 추가될 때마다 자료형에 *이 하나씩 늘어나게 됩니다.

728x90

UART핀 찾기
UART

그림에서는 UART가 3핀으로 구성되어 있지만, 보통은 VCC와 같이 4핀으로 구성되어 있는 경우가 많습니다.

위 사진에서 보면 4핀이 연속으로 구멍이 뚫려있는 곳이 있다. 찾기 힘들면 기판에 RX, TX 가 써져있는 것을 찾으면 됩니다.

노란색 줄 친 부분이 GND, TX, RX이고 빨간 원 친 부분 전체가 UART 홀입니다.

(참고로 보드에 써져있는 0BA440은 기판 색입니다.)

 

아래 써져있는 스펙을 기준으로 찾아본 결과 가장 비슷하게 생긴건 NEXT-606N 인것 같습니다.

NEXT-606N

첫 번째 사진

칩에 써져있는 정보대로 MediaTek 사이트에 검색해본 결과

www.mediatek.com/products/homenetworking/mt7628k-n-a

 

MT7628K/N/A

MT7628K/N/A 2x2 802.11n platform for N300/AC750/AC1200 router and repeater MediaTek MT7628 family are the new generation of 2T2R 802.11n Wi-Fi AP/Router (SoC). MT7628 improve the RF performance, reduce power consumption and optimize the BOM cost and make i

www.mediatek.com

N300 / AC750 / AC1200 라우터 및 리피터 용 SoC(System on a Chip)인 것을 알 수 있습니다.

상세 펙은 이렇게 되는 것 같습니다.

MT7628

MIPS 아키텍쳐를 사용하는 싱글코어 32비트 575/580MHz 클럭의 cpu가 달려있고

16bit DRAM 2개에 I2S와 PCM을 사용하는 오디오 인터페이스

소프트웨어론 eCos, Linux 2.6.36, 3.10 그리고 OpenWRT를 사용할 수 있습니다.

IEEE 802.11n(WiFi 4)의 와이파이를 쓸 수 있고 2.4GHz 주파수를 쓸 수 있네요.

그리고 2개의 송수신 안테나를 달 수 있는것 같습니다.

5포트짜리 Fast Ethernet(100Mbit/s) 스위치를 쓸 수 있고 1개의 포트를 IoT 용으로 쓸 수 있는것 같습니다.

I/O 는 GPIO와 UART가 있네요. 아마 GPIO는 LED에 사용하지 않았을까 합니다.

 

좀 더 상세한 데이터 시트는 여기 있습니다.

vonger.cn/upload/MT7628_Full.pdf

 

두 번째 사진

W9751G6KB-25를 검색해본 결과

www.ariat-tech.kr/parts/winbond-electronics-corporation/W9751G6KB-25

512MBit => 64MB의 SDRAM인 것을 확인할 수 있었습니다.

상세 스펙은 이렇게 되는 것 같습니다.

512Mbit(64MB) SDRAM(Synchronous Dynamic RAM)이고 8x12.5 BGA(Ball Grid Array) 방식이며 속도(클럭)는 200MHz 인것 같습니다. 인터페이스가 Parallel 이라고 하는데 데이터 시트를 찾아본 결과 SSTL 인터페이스에 1.8V를 사용합니다.

8x12.5 BGA

세 번째 사진

잘 보이진 않지만 25032JVS10 혹은 25Q32JVSIQ 인 듯합니다.

그래서 검색해본 결과 정확히 같은 모델명은 나오지 않았지만

datasheetspdf.com/pdf/1330826/Winbond/W25Q128JV/1

kr.mouser.com/ProductDetail/Winbond/W25Q64JVSSIQ?qs=qSfuJ%252Bfl%2Fd4DecxlHDmB8w%3D%3D

등 32가 64로 바뀐듯한 모습의 IC들이 나왔고, 다른 데이터 시트를 찾아본 결과

위쪽에 마킹되는 문구에 맨 앞에 W랑 S가 두 개 들어가서 일련번호로 바뀌는 것을 알 수 있었습니다.

그러면 25Q32JVSIQ를 일련번호로 변경하면 W25Q32JVSSIQ 가 되고 검색해보니

pdf1.alldatasheet.co.kr/datasheet-pdf/download/932073/WINBOND/W25Q32JVSSIQ.html

데이터 시트를 발견할 수 있었습니다. 다운해보니

1. GENERAL DESCRIPTIONS The W25Q32JV (32M-bit) Serial Flash memory provides a storage solution for systems with limited space, pins and power. The 25Q series offers flexibility and performance well beyond ordinary Serial Flash devices. They are ideal for code shadowing to RAM, executing code directly from Dual/Quad SPI (XIP) and storing voice, text and data. The device operates on 2.7V to 3.6V power supply with current consumption as low as 1µA for power-down. The W25Q32JV array is organized into 16,384 programmable pages of 256-bytes each. Up to 256 bytes can be programmed at a time. Pages can be erased in groups of 16 (4KB sector erase), groups of 128 (32KB block erase), groups of 256 (64KB block erase) or the entire chip (chip erase). The W25Q32JV has 1,024 erasable sectors and 64 erasable blocks respectively. The small 4KB sectors allow for greater flexibility in applications that require data and parameter storage. (See Figure 2.) The W25Q32JV supports the standard Serial Peripheral Interface (SPI), and a high performance Dual/Quad output as well as Dual/Quad I/O SPI: Serial Clock, Chip Select, Serial Data I/O0 (DI), I/O1 (DO), I/O2, and I/O3. SPI clock frequencies of up to 133MHz are supported allowing equivalent clock rates of 266MHz (133MHz x 2) for Dual I/O and 532MHz (133MHz x 4) for Quad I/O when using the Fast Read Dual/Quad I/O instruction

32M-bit => 4MB 크기의 플래시 메모리이며 SPI 인터페이스를 사용하는 것을 알 수 있었습니다.

 

종합해 보자면

1. SoC으로 계산이랑 여러 가지 종합적인 것을 담당하고

2. DRAM으로 주기억 장치 역할을 하고

3. 플래시 메모리로 보조기억장치 역할을 한다는 것을 알 수 있습니다.

728x90

부트로더

컴퓨터(또는 MCU)의 전원이 켜질 때 일반적으로 램(RAM)에 운영 체제가 없습니다. 그래서 컴퓨터는 적은 양의 필요한 데이터와 함께 롬(ROM)에 저장된 작은 프로그램을 실행하여 운영체제를 보조 기억 장치에서 RAM에 로드합니다.

이 작업을 하는 작은 프로그램 부트 로더라고 합니다.

 

기능

1. 직렬 통신, 인터럽트, 타이머, 콘솔, 메모리, 각종 입출력 장치 등을 점검하여 실행 가능 상태로 만들어 줍니다.

2. 화면에 내용을 출력해서 사용자 인터페이스 기능을 합니다.

3. 커널을 메모리(RAM)에 로드시켜서 사용자 명령 처리를 준비하는 역할을 합니다.

 

부트로더의 종류

  • U-Boot(Universal Bootloader)
    • PPCBoot와 ARMBoot 프로젝트 기반
  • LILO(Linux Loader)
  • GRUB(Grand Unfied Bootloader)
  • Loadlin
  • EtherBoot
  • Blob
    • ARM용 부트로더
  • PMON(PROM Monitor)
    • MIPS 보드용
  • RedBoot
    • RedHat에서 개발 및 배포
    • eCos 기반

 

커널

운영체제의 핵심으로 하드웨어와 응용프로그램 사이에서 보안, 자원관리, 추상화를 해줍니다.

파일 시스템

운영체제에서 파일이나 자료를 쉽게 접근할 수 있도록 보관 또는 조직하는 체계

 

아이노드(inode): UFS와 같은 유닉스 계통 파일 시스템에서 사용하는 자료구조이며, 정규 파일, 디렉터리  파일 시스템에 관한 정보를 가지고 있습니다.

 

파일 시스템 내에서 파일이나 디렉토리는 고유한 inode 를 가지고 있으며, inode 번호를 통해 구분이 가능합니다.

 

컴퓨터에서 파일을 저장할 때 하드디스크에 연속적으로 저장하는것이 아닌 파일들을 조각으로 나눠서 저장하게 되는데, 그 파일들이 어디있는지를 기록해 둬야 합니다.

파일 시스템 종류

FAT(File Allocation Table)

볼륨의 맨 위에 FAT 이라는 테이블이 있고 그 테이블에 파일의 할당 정보를 저장합니다.

파일을 섹터 단위로 나누면 파일이 너무 작게 나눠지기 때문에 적당히 작게 나누기 위해서 클러스터를 사용합니다.

FAT 뒤에 숫자는 클러스터 ID를 저장하는데 사용하는 비트 수 입니다. 그래서 최대 표현 가능한 클러스터 수와 연관이 되어 있습니다.

 

NTFS(New Technology File System)

윈도우 NT부터 기본적으로 사용되는 파일 시스템으로, 클러스터 ID를 저장하는데 64비트를 사용합니다.

FAT과 달리 테이블을 사용하지 않고 B+ tree를 사용하여 탐색 속도가 빨라졌습니다.

그리고 아래에 나오는 저널링 파일 시스템을 사용하여 안정성이 높아졌습니다.

파일을 압축하여 보관하거나, 접근 권한을 제어하는 등 여러가지 기능이 생겼습니다.

하지만 윈도우만 생각하고 만들어서 호환성이 좋진 않습니다.

 

exFAT(EXtended File Allocation Table)

NTFS의 호환성 문제를 해결하기 위해서 만들어진 파일 시스템입니다.

FAT 에서 클러스터 ID를 저장하는데 사용하는 비트 수가 64비트로 바뀌었습니다.

 

EXT(EXTended file system)

리눅스 운영 체제를 위해서 만들어진 파일 시스템입니다.

Extended File System을 줄여 extfs 또는 ext로 씁니다.

부트섹터와 여러개의 블록 그룹으로 이루어져 있습니다.

부트섹터에는 부팅에 필요한 정보들이 담겨있고, 블록은 클러스터와 비슷한 역할을 수행합니다.

블록 그룹은 Super Block, Group Descript Table(GDT), Block Bitmap, Inode Bitmap, Inode Table, Data의 구조로 구성되어 있습니다.

 

EXT2, 정식 이름은 Second Extended File System입니다. 이 파일 시스템은 ext의 문제를 해결하기 위해 나온 파일 시스템입니다.

ext2 파일 시스템은 255자까지의 긴 파일 이름을 지원합니다. ext2 파일 시스템은 세 타임스탬프를 지원하며, 확장이 쉽습니다. 그리고 ext에 있었던 여러 단점(분리 접근, 아이노트 수정 등 지원 안 함)도 개선되었습니다. 파일 시스템의 최대 크기는 블록 사이즈에 따라 2 TiB ~ 32 TiB이며, 서브 디렉터리 개수 제한은 32768개입니다.

EXT3ext2에 저널링 파일 시스템, 큰 디렉터리를 위한 HTree 인덱싱 등의 기능이 추가되었습니다. ext3는 ext2를 바탕으로 만들었기 때문에, ext2 파일 시스템을 자료 손실 없이 ext3 파일 시스템으로 바꿀 수 있습니다.

 

EXT4

64비트 기억 공간 제한을 없애고 ext3의 성능을 향상하며, 하위 호환성이 있는 확장 버전으로서, 많은 부분이 본래, 러스터 파일 시스템을 위해 클러스터 파일 시스템스사에서 개발되었습니다. 여러가지 장점이 있지만 너무 길기 때문에 작성하지 않겠습니다.

 

JFFS(Journalling Flash File System)

파일 시스템에 반영될 변경 사항을 주기적으로 로그로 남겨 비정상 종료가 발생하면 저널은 저장되지 않은 정보를 복구하기 위한 검사 지점으로 사용되어 파일 시스템 메타자료 손상을 막아줍니다.

즉, 일정부분을 기록을 위해 남겨두고, 그 부분에 기록을 남겨 백업 및 복구 능력이 있는 파일 시스템을 말하며,

시스템 충돌 후에 파일시스템 복구에 드는 시간이 획기적으로 줄어듭니다.

 

JFFS2

이름부터 JFFS2 인 만큼 JFFS에서 여러가지 새로운 점이 추가가 되었습니다.

1. NAND 플래시 지원

2. zlib, rubin, rtime 이 세 종류 압축 알고리즘 지원

3. 성능 향상

 

YAFFS(Yet Another Flash File System)

NAND 플래시를 위해서 설계된 파일시스템 입니다.

JFFS가 모든 페이지에 대해서 데이터를 읽어야 한다는 초기화 알고리즘의 단점을 극복하기 위해 파일의 메타데이터인 아이노드를 한 페이지에 저장하는 방법을 사용하고 있습니다.

 

UBIFS(UBI File System)

F2FS(Flash-Friendly File System)

LogFS

등이 있습니다.

728x90

추측 실행(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

+ Recent posts