이번에는 다음과 같은 예시 명령어를 보면서 메모리 리오더링에 대해 자세히 알아봅시다.

 

그림 17.4 메모리 리오더링의

 

01 ~ 04번째 줄은 모두 스토어 명령어입니다. 01 ~ 04번째 명령어가 실행될 , X1 레지스터의 값이 0x10000이라고 가정합시다. 또한 명령어가 실행될 워드의 단위는 8바이트입니다.

 

먼저 01번째 줄을 분석하겠습니다. X1 레지스터가 0x10000이니 0x10000 주소에 X0 레지스터의 (8바이트 크기) 저장합니다. 01번째 줄이 실행되면 바이트는 '1' 업데이트됩니다. 이는 '1' 표기된 박스에 해당합니다. 이어서 02번째 줄을 봅시다. 같은 0x10000 주소에 2바이트 사이즈의 데이터를 저장합니다. 여기서 01번째 줄에서 실행된 0x10000 주소의 데이터가 업데이트 됩니다. 부분은 2 표기된 박스에서 확인할 있습니다. 여기서 STRH 명령어는 2바이트만큼 데이터를 스토어하는 동작이므로 0x10000 0x10001 주소에 데이터(‘2’ 표시된 박스) 저장합니다.

 

이어서 03번째 줄을 봅시다. 0x10002 주소에 4바이트 사이즈의 데이터를 저장합니다. 이는 가운데 3으로 표기된 박스에 해당됩니다. 03번째 줄의 명령어가 STR이고 저장하는 W3 레지스터의 사이즈가 4바이트입니다. 따라서 0x10002 ~ 0x10005 주소에 W3 레지스터의 값이 저장됩니다. 여기서 다음과 같은 질문을 던지면서 예제 명령어에 대해 조금 생각해 봅시다.

 

    "02번째와 03번째 명령어 사이에 의존성이 있을까"?

 

질문에 답하려면 02번째와 03번째 명령어가 연달아 실행되면 어느 주소 공간에 데이터가 저장되는지 확인해야 합니다. 02번째 줄은 0x10000 ~ 0x10001 영역 주소에 2바이트 크기만큼 데이터(2) 저장하고, 03번째 줄은 0x10002 ~ 0x10005 구간 주소에 4바이트 사이즈 데이터(3) 저장합니다.

 

02번째와 03번째 줄을 실행한 결과 업데이트되는 주소 공간이 중복되지 않습니다. 따라서 "02 ~ 03번째 명령어 간에는 의존성이 없다"라고 답할 있습니다. 만약 Arm 프로세서 내부에서 03번째 명령어를 먼저 실행하고 02번째 명령어를 실행하면 결과가 달라지지 않습니다. 그래서 명령어의 순서를 바꿔서 실행할 있습니다. 이를 메모리 리오더링이라고 합니다.

 

여기서 걸음 나갈까요? Arm 프로세서는 내부에서 02 ~ 03번째 명령어를 번에 실행할 수도 있습니다. 이를 Arm 아키텍처에서는 머지 액세스라고 하는데 이번에 예시 명령어를 통해 머지 액세스가 무엇인지 자세히 알아봅시다.

 

02번째 줄은 0x10000 ~ 0x10001 범위의 주소에 접근합니다. 이를 메모리 액세스를 한번 수행했다고 말할 있습니다. 03번째 줄은 0x10002 ~ 0x10005 범위의 주소에 접근하며 역시 메모리 액세스를 처리했다고 있습니다. 02 ~ 03번째 줄을 순서대로 실행하면 2 메모리 액세스를 수행합니다.

 

만약 02번째 줄과 03번째 줄을 번에 실행하면 어떻게 될까요? 02 ~ 03번째 명령어 사이에 의존성이 없으니 번에 0x10000 ~ 0x10005 메모리 영역에 접근해 데이터를 스토어할 있습니다. 이를 머지 액세스라고 합니다.

 

 

[정보] 메모리 리오더링의 예시

 

명령어가 순차적으로 실행해도 프로세서 내부에서 메모리에 접근하는 순서는 순차적이지 않을 수도 있습니다.

 

이해를 돕기 위해 가지 예를 들겠습니다. 은행에 먼저 고객은 1 번호표를 받고 은행 직원과 상담을 시작했습니다. 그런데 다음에 2 번호표를 받은 고객이 창구에 왔는데, 은행 직원이 봤을 1번보다 2 번호표의 고객을 은행 직원이 먼저 처리할 있다고 판단합니다. 1 번호표의 고객의 요청 사항을 처리하려면 은행의 인프라 서버에 10 간격으로 접근할 있는 상황이기 때문입니다. 무엇보다 1 고객의 요청이 2 고객과 의존성이 없는 상황이니 고민할 필요가 없습니다. 이럴 2 번호표의 고객을 은행 직원이 먼저 처리할 있습니다.

 

1 고객과 2 고객의 요청을 모아 은행의 인프라 서버에서 처리하면 업무 시간을 줄일 수도 있습니다. 은행 직원도 메모리 리오더링과 유사한 방식을 적용해 업무를 최적화합니다.

 

 

Arm 프로세서 내부에서 성능을 최적화하기 위해 종종 어셈블리 명령어의 순서를 바꿔 실행한다고 설명했습니다. 이런 동작을 수행하기 전에 먼저 각 명령어 간에 의존성이 있는지 체크합니다. 명령어 간 의존성이 있다면 명령어의 순서를 지켜 실행합니다.

다음 어셈블리 명령어를 보면서 어드레스 의존성에 대해 배워 봅시다.
 

그림 17.2 의존성이 있는 로드(LDR) 명령어의 예시

위 코드는 2개의 로드 명령어로 구성돼 있습니다. 

먼저 01번째 줄을 봅시다. X1 레지스터가 0x30000을 담고 있고 0x30000 주소에 0x10000이 있다고 가정합시다. 01번째 줄은 X1 레지스터가 담고 있는 주소에 있는 데이터를 X0 레지스터에 로딩하는 동작입니다. 이어서 02번째 줄은 X0 레지스터의 값에서 8을 더한 결과(주소)에 있는 값을 X2 레지스터에 로딩합니다. 

01번째 줄 명령어를 실행한 결과, X0이 0x10000이면 02번째 줄은 0x10008 주소에 있는 데이터를 X2 레지스터에 로딩합니다. 02번째 줄은 다음과 같이 실행된다고 볼 수 있습니다.

X2 = *(0x10000 + 0x8);

이번에는 다른 예를 들까요? 01번째 줄 명령어의 실행 결과 X0이 0x20000이면 02 번째 줄은 어떻게 실행될까요? 02번째 줄은 0x20008 주소에 있는 데이터를 X2 레지스터에 로딩합니다. 이를 다음과 같은 수식으로 나타낼 수 있습니다. 

X2 = *(0x20000 + 0x8);

앞의 두 가지 예시에서 든 명령어의 공통점은 다음과 같은 의사 코드로 표현할 수 있습니다. 

X2 = *(0x0 + 0x8);

“X2 레지스터의 값은 X0 레지스터에 따라 결정된다”라고 해석할 수 있습니다. 

앞에서 소개한 두 가지 패턴의 명령어에서 한 가지 기억할 점은 "01번째 줄 명령어의 실행 결과가 02번째 줄에 영향을 미친다."라는 것입니다. 여기서 X0 레지스터의 값에 따라 02번째 줄의 실행 결과에 영향을 미치니 “01번째와 02번째 줄 명령어 사이에 의존성이 있다”라고 볼 수 있습니다. 이를 어드레스 의존성(address dependence) 혹은 레지스터 의존성(register dependence)이라고 하며, 다음 그림과 같이 표현할 수 있습니다.

 

그림 17.3 어드레스 의존성

만약 의존성이 있는 01번째 줄과 02번째 줄 명령어의 실행 순서를 바꾸면 어떻게 동작할까요? 명령어가 실행되면 예측할 수 없는 이상한 값이 레지스터에 저장될 가능성이 높아 오동작을 유발할 것입니다. 따라서 명령어 사이에 의존성이 있으면 Arm 프로세서 내부에서 메모리에 액세스하는 순서를 바꾸지 않습니다.

대부분 시스템 소프트웨어 개발자는 C 언어로 프로그래밍합니다. C 언어로 작성된 코드는 컴파일러에 의해 기계어로 변환되는데, 과정에서 컴파일러는 성능을 최적화하기 위해 코드를 나름대로 재해석합니다. 컴파일러에 의해 최적화된 코드를 보면 " 컴파일러는 똑똑하구나, 어떻게 코드를 알아서 생성했을까!" 생각이 듭니다.

 

C 코드를 컴파일할 컴파일러 최적화 옵션을 적용하는데, 최적화 레벨이 높을수록 컴파일러는 높은 수준의 최적화를 수행합니다. 하지만 컴파일러는 프로그래머가 작성한 코드를 최적화하는 과정에서 종종 사고를 치기도 합니다. 코드를 최적화하다 보니 선을 넘어 프로그래머가 의도하지 않은 코드를 생성하기 때문입니다. 예를 들어, 함수를 인라인으로 처리하면서 심벌이 제거되기도 합니다.

 

컴파일러가 코드를 생성하는 과정에서 최적화를 수행해 문제를 일으키면 차라리 낫습니다. C 코드와 컴파일러가 생성한 어셈블리 명령어를 비교하면 컴파일러가 명령어를 어떻게 생성했는지 확인할 있기 때문입니다. 하지만 Arm 프로세서 내부에서 명령어의 순서를 바꿔서 실행하면 소프트웨어 개발자가 이를 눈으로 확인할 없습니다. Arm 프로세서는 명령어 의존성이 없다면 성능을 최적화하기 위해 '메모리 액세스' 순서를 변경할 있기 때문입니다.

 

이러한 노멀 메모리의 특징을 Weakly-Ordered 모델이라고 명시합니다. 'weakly Ordered 모델 가장 특징은 메모리 액세스 순서가 프로그램 순서와 같을 필요가 없다라는 점인데, 여기서 말하는 프로그래밍 순서의 기준은 로드와 스토어입니다.

지금까지 Arm 아키텍처에서 정의된 노멀 메모리 타입과 디바이스 메모리 타입을 알아봤습니다. 이어서 메모리 맵을 보면서 노멀 메모리 타입과 디바이스 메모리 타입의 특징을 자세히 알아봅시다.

[정보] 메모리 맵이란?

실전 프로젝트를 진행하면 메모리 맵이란 용어를 자주 듣습니다. 메모리 맵은 무엇일까요? 프로그램에 의해 실행되는 프로세스 입장에서 바라 본 메모리 레이아웃입니다. 지도를 보면 일정한 규칙(시/도)에 따라 구획이 나눠져 있듯이 메모리 영역을 속성별로 분류한 일종의 메모리 지도가 메모리 맵입니다. 


메모리 맵의 구조도 알아보기

메모리 맵은 데이터 영역, 코드와 같은 속성으로 마킹된 다양한 영역으로 구성돼 있습니다. 다음 그림을 보면서 메모리 맵에 대해 더 자세히 알아봅시다.

 
출처: Learn the architecture - ARMv8-A memory systems https://developer.arm.com/documentation/100941/latest/

그림 17.1 메모리 맵의 구조도 

그림 17.1을 보면 가장 왼쪽 열에는 메모리 구간별 주소를, 가운데에는 영역 이름, 그리고 가장 오른쪽에는 메모리 속성을 확인할 수 있습니다.

메모리 맵의 가장 윗부분에 있는 Peripheral부터 봅시다. Peripheral로 분류되는 메모리 영역은 디바이스 메모리 타입으로 분류됩니다. 이 영역에 있는 데이터는 오로지 읽고/쓰기만 가능하며 직접 실행할 수는 없습니다. Peripheral은 프로그래머들이 작성한 코드가 로딩될 수 없는 영역이기 때문입니다. 

그다음으로 가운데에 있는 Kernel Code를 봅시다. Kernel Code에 있는 데이터는 실행만 가능하며 읽고/쓰기는 불가능합니다. 운영체제 커널 코드가 로딩된 영역이므로 코드를 읽거나 쓰면 코드 내용이 바뀌므로 읽고/쓰기는 불가능하도록 설정합니다. 

이어서 바로 위에 있는 Kernel Data를 봅시다. Kernel Data에 있는 데이터는 오로지 읽고/쓰기만 가능하며 직접 실행할 수는 없습니다.  

여기서 주의깊게 봐야 할 점이 있습니다. Kernel Data와 Kernel Code의 공통적인 속성은 Cacheable, Privileged입니다. 둘 다 캐시에 의해 처리되며 Privileged 권한으로 접근됩니다. 또는 Privileged는 Armv8 기준으로 EL1 이상의 권한으로 실행되는 동작 권한을 뜻합니다.

마지막으로 가장 아랫부분에 있는 App Data와 App Code의 영역을 알아봅시다. Kernel Data와 Kernel Code 영역이 지닌 속성과 대부분 같지만 Unprivileged라는 속성이 다릅니다. Unprivileged(비특권)는 가장 낮은 실행 권한을 뜻하며, Unprivileged 권한으로 실행되는 코드는 Privileged 권한이 부여된 코드나 데이터에 직접 접근할 수 없습니다.

메모리 맵을 구성하는 주요 영역

앞에서 든 예시와 같이 메모리 맵은 메모리 주소별 영역의 이름과 각 속성 정보로 구성돼 있습니다. 그림에서 설명한 내용 이외에 메모리 맵은 다음과 같은 속성 정보로 구성돼 있습니다.

 메모리 페리페럴
 메모리 영역의 코드와 데이터
 운영체제와 유저 애플리케이션에 포함되는 리소스

그림 17.1을 보면 다양한 메모리 영역이 존재하나 Arm 아키텍처 관점에서 보면 메모리 타입은 크게 두 가지로 분류됩니다.

 디바이스 메모리: Peripheral  
 노멀 메모리: Kernel Code, Kernel Data, App Code, App Data 
 
Kernel Code, Kernel Data, App Code, App Data 주소 영역은 노멀 메모리로 관리하며 페리페럴은 디바이스 메모리(메모리 맵드 I/O)로 처리됩니다.

메모리 맵에 존재하는 노멀 메모리 타입과 디바이스 메모리 타입을 구분하는 가장 큰 차이점은 무엇일까요? 바로 명령어나 메모리를 리오더링하는 동작입니다. 노멀 메모리 타입으로 분류된 명령어나 데이터를 Arm 코어가 실행하면 내부에서 다양한 방식으로 최적화 작업을 수행합니다. 대표적으로 메모리 접근 순서(메모리 리오더링)나 명령어의 실행 순서를 바꿉니다. 소프트웨어 개발자가 작성한 대부분의 코드는 노멀 메모리 타입으로 분류되며, 명령어와 데이터는 이 방식으로 처리됩니다. 

디바이스 메모리를 처리하는 방식

Arm 프로세서가 디바이스 메모리를 처리하는 방식은 노멀 메모리를 처리하는 방식과 다릅니다. 디바이스 메모리로 분류된 영역의 데이터를 Arm 코어가 보면 메모리 리오더링와 같은 기법을 적용해 실행하지 않습니다. 디바이스 메모리 타입은 메모리 맵드 I/O 방식이며, 대부분 페리페럴은 디바이스 메모리 타입으로 관리됩니다. 메모리 맵드 I/O로 정의된 주소에 접근하는 한 가지 예를 들까요?

 0xD000_0000 주소 접근: 페리페럴에 인터럽트 활성화
 0xD000_0004 주소 접근: 페리페럴에 인터럽트를 잘 받았다는 ACK(Acknowledge)를 전달
 0xD000_0008 주소 접근: 페리페럴의 인터럽트를 비활성화

위 목록은 메모리 맵드 I/O 기반 물리 주소와 특징을 나타냅니다. 각각 드라이버에서 0xD000_0000, 0xD000_0004, 0xD000_0008 주소에 접근해 페리페럴을 제어할 수 있습니다.

0xD000_0000 주소와 0xD000_0008 주소에 순차적으로 접근해 페리페럴을 제어하려는 루틴이 있는데, 이를 Arm 코어가 노멀 메모리 타입으로 처리하면 주소에 접근하는 순서를 바꿔서 실행할 수 있습니다. 예를 들어 페리페럴에서 인터럽트를 받으면 인터럽트에 대한 ACK를 보내고 페리페럴의 인터럽트를 비활성화하는 명령어를 작성했는데, 명령어의 순서가 바뀌면 오동작할 수 있습니다. 즉, 디바이스 메모리 타입으로 분류되는 영역이 노멀 메모리 타입처럼 처리되면 이처럼 예상치 못한 사이트 이펙트가 유발될 수도 있습니다.

이어서 디바이스 메모리를 알아봅시다. 메모리 맵을 보면 다양한 페리페럴 디바이스의 세부 정보와 함께 레지스터를 있습니다. 이를 Memory-Mapped I/O 혹은 디바이스 메모리라고 합니다.

 

디바이스 메모리 타입은 노멀 메모리 타입과 어떤 차이점이 있을까요? Arm 프로세서 입장에서 노멀 메모리로 마킹된 데이터는 메모리에 접근하는 명령어의 순서를 바꾸는 메모리 리오더링 기법이 적용됩니다. 하지만 디바이스 메모리인 경우는 다릅니다. 디바이스 메모리로 마킹된 데이터에 액세스할 메모리 리오더링이 적용되면 프로그래머가 예상하지 못한 오동작이 발생할 있습니다. 그래서 디바이스 메모리로 마킹된 데이터는 명령어를 순차적으로 실행해야 합니다.

 

디바이스 메모리는 페리페럴을 제어하는 용도로 사용되므로 명령어는 디바이스 메모리 타입으로 마킹된 메모리 영역에 로딩되면 됩니다. 그래서 디바이스 메모리로 지정된 영역은 실행 불가(not executable) 지정돼야 합니다. 디바이스 메모리 영역으로 지정된 공간을 '실행 불가' 설정하지 않으면 프로세서는 명령어를 메모리에서 미리 예측해 로딩할 수도 있어 결과 시스템이 예상치 못한 오동작을 있습니다.

명령어를 실행하거나 명령어 실행 과정에서 처리되는 데이터나 코드는 노멀 메모리 타입으로 처리됩니다. 대부분의 소프트웨어 개발자가 입력한 코드는 노멀로 마킹된 메모리 영역에 로딩된다고 있습니다.

 

Arm 프로세서는 노멀 메모리로 마킹된 영역의 데이터를 처리하기 위해 캐싱 기법을 적용합니다.

CPU 노멀 메모리로 마킹된 메모리 영역에 대해 다음과 같은 기법을 적용해 최적화를 수행합니다.

 

l  머지 액세스

l  스페큘레이션(speculation)

l  리오더링 액세스

 

노멀 메모리를 처리하는 과정에서 적용되는 다양한 기법을 소개합니다.

 

머지 액세스

 

메모리 공간에 여러 접근하거나 연속된 메모리 공간에 접근하는 2 이상의 명령어를 번에 처리하는 동작을 머지 액세스(merge access)라고 합니다.

 

CPU 입장에서는 레지스터보다 메모리에 접근할 많은 사이클과 리소스를 사용합니다. 이유는 무엇일까요? 메모리에 접근하려면 메모리 버스에 먼저 접근해야 하며, DRAM 메모리 콘트롤러와 같은 메모리 하드웨어에서 부가적인 동작을 수행하기 때문입니다. 메모리에 액세스하는 횟수가 늘어날수록 이에 비례해 많은 사이클을 소모합니다. 따라서 메모리에 접근하는 횟수를 줄이면 시스템 성능을 높일 있습니다.

 

Arm 프로세서 내부에도 메모리에 액세스하는 횟수를 최소화해 성능을 높이는 메커니즘이 적용돼 있습니다.  예를 들어, 같은 주소 공간에 있는 메모리에 액세스하는 명령어가 있으면 이를 합쳐서 번만 메모리에 접근할 있습니다.

 

스페큘레이션 액세스

 

성능 최적화를 위한 다른 CPU 설계 기법은 스페큘레이션 액세스입니다. 소프트웨어에서 접근할 것으로 예상하는 데이터를 패턴 인식과 같은 알고리즘을 활용해 미리 예측해 미리 로딩하는 동작을 스페큘레이션 액세스라고 하며 이를 통해 성능을 높일 있습니다.  

 

밖에도 Arm 프로세서는 내부에서 분기 예측, 실행 예측을 통해 성능을 극대화하는  메커니즘이 적용되어 있습니다.

 

리오더링 액세스

 

Arm 프로세서는 성능을 향상하기 위해 프로세서 내부에서 명령어 실행 순서를 바꿔서 실행할 있습니다. 이를 위해 Arm 프로세서는 명령어 간에 의존성이 있는지 체크하며, 의존성이 없는 경우 명령어의 순서를 바꿔 처리합니다.

 

Arm 프로세서는 내부적으로 성능을 극대화하기 위해 다양한 기법을 적용해 왔습니다. 가운데 대표적인 기법은 "데이터 처리량을 높이기 위해 메모리 읽기 혹은 쓰기 작업을 재정렬(reorder)하는 동작"으로, 이를 메모리 리오더링이라고 합니다. 메모리 리오더링을 통해 프로세서와 외부 메모리 간의 데이터 처리량을 늘릴 있고, 결과 프로세서의 성능을 높일 있습니다.

 

앞에서 언급한 가지 동작 특성은 노멀 메모리 타입으로 마킹된 메모리 영역에 적용됩니다. 일반적으로 대부분 소프트웨어 개발자가 입력한 코드는 앞에서 언급한 가지 특징이 적용된 상태에서 실행됩니다.

 

 

[정보] 분기 예측과 실행 예측

 

최신 Arm 코어는 성능을 높이기 위해 분기 예측이나 실행 예측을 수행하기도 합니다. 이러한 CPU 코어의 특징을 악용해 해커들은 악의적인 코드나 데이터를 캐시에 남기려는 시도를 합니다. 이를 스펙터(spectre) 혹은 멜트다운(meltdown)이라고 합니다.

 

 

 

앞에서 설명한 특징을 가리켜 노멀 메모리 타입은 "Weakly ordered"라는 특징이 있다고 말합니다. 여기서 말하는 "Weakly ordered" 무엇일까요? 프로그램을 실행하면 메모리에 액세스하는데, 메모리 액세스 순서가 프로그램에 명시된 프로시저(실행 흐름) 같지 않다는 의미입니다.

트러스트존의 유래를 설명하기에 앞서 해커가 시스템에 침투해 해킹하는 것을 방지하기 위해 시스템을 설계하는 과정을 언급할 필요가 있습니다.

 

2000 초부터 다양한 소프트웨어 기술들이 소개되고 발전했는데, 이에 비례해 해킹 기술도 업그레이드됩니다. 해커들이 다양한 소프트웨어 기법을 활용해 시스템을 공격하면서 해킹의 공격 루트가 다양해집니다. 디바이스 드라이버를 시작으로 네트워트 패킷이나 서버를 통해 시스템을 침입하는 경로가 늘어났습니다.

 

많은 IT 업체들이 해킹으로 피해를 보면서 보안 환경을 구축할 있는 보안 시스템의 필요성을 절감하기에 이르렀습니다. 그래서 IT업체들은 중요한 데이터를 암호화하거나 해커의 공격으로부터 보호할 있는 시스템을 설계합니다. 고육지책으로 보안과 관련된 기능을 제공하는 하드웨어 부품을 시스템에 탑재합니다. 같은 노력으로 중요한 데이터는 보호할 있으나 회로 설계나 소프트웨어의 복잡도가 늘어나고, 결과로 제품의 가격이 높아집니다.

 

결국 CPU 아키텍처에서 보안과 관련된 기능(데이터 암호화, 복호화) 제공해 처음부터 보안을 고려해 시스템을 설계할 있는 환경이 구축되면 좋겠다는 요구사항이 생겼습니다. 특히 SoC 벤더나 보안 업체가 효율적으로 보안 환경을 구축할 있는 아키텍처가 필요했습니다. 또한  소프트웨어가 공격 당할 있는 취약점이 너무 다양한데, 이를 어떻게 방어할지에 대해 고민했습니다.

 

고민 끝에 아예 보안 수준을 높인 운영체제(Trusted OS) 실행될 있는 보안 실행 영역을 정의해 해커로부터 받는 공격 포인트를 하나로 줄이자는 방향으로 시스템 아키텍처를 설계했고, 과정에서 트러스트존이 만들어졌습니다.

 

그럼 트러스트존은 언제 처음 배포됐을까요? 트러스트존은 2004 Arm1176JZ-S™ 프로세서에 탑재됐으며, 현재 Cortex-A 제품군에 포함돼 있습니다.

 

 

[정보] TEE(Trusted Execution Environment)란?
TEE는 프로세서 내부에서 보안 수준이 높은 소프트웨어 플렛폼이 실행될 수 있는 환경을 뜻합니다. 이를 위한 다양한 표준을 세워야 하는데 이를 TEE Committee 단체에서 진행합니다.  

 

.

 

 

 

많은 분들이 생각하는 익셉션의 개념은 'Armv7 아키텍처의 익셉션'인 경우가 많아, Armv8 아키텍처를 배울 때 Armv7 아키텍처의 익셉션과 비슷할 것이라 예상합니다. 익셉션이 발생하면 지정한 주소로 프로그램 카운터를 브랜치하는 기본 개념은 Armv7/Armv8 아키텍처가 같으나, 익셉션을 처리하고 분류하는 체계와 세세한 처리 방식이 많이 다릅니다.

Armv8 아키텍처의 익셉션은 고성능 컴퓨터에서 적용되는 트러스트 존이나 가상화 시스템인 하이퍼바이저를 이해하기 위해 반드시 알아야 할 기반 지식이므로, 반드시 잘 알아야 둬야 합니다. 

익셉션(Exception)은 Armv8 아키텍처의 핵심 기능 중 하나입니다. Armv8 아키텍처에서 정의된 익셉션를 활용해 하이퍼바이저와 같은 가상 시스템을 설계할 수 있습니다. 또한 트러스트 존을 제대로 이해하려면 먼저 익셉션의 동작 방식을 알아야 합니다. 

Armv8 아키텍처에서 익셉션은 어떻게 정의내릴 수 있을까요? 기존에 소개된 Armv8 아키텍처의 익셉션과는 어떤 차이점이 있을까요? Armv8 아키텍처에서는 기존 Armv7 아키텍처와 비교해 익셉션의 종류를 분류하는 방식이 조금 다르지만, 익셉션의 동작 원리는 동일합니다. Armv8 아키텍처의 익셉션은 다음과 같이 설명할 수 있습니다.

 

“익셉션이란 Arm 코어가 명령어를 처리하다가 예외 사항이 발생할 때 이를 처리하는
방식이다. 익셉션이 발생하면 익셉션 벡터로 프로그램 카운터가 브랜치된다.”

 


위 문장을 읽으면 "어, Armv8 아키텍처의 익셉션은 Armv7 익셉션과 거의 같네"라는 생각이 들수 있습니다. 하지만 Armv8 아키텍처의 익셉션은 Armv7 익셉션과 비교했을 때 기본 개념은 같지만 익셉션을 분류하고 처리하는 방식이 다릅니다.

이제부터 Armv8 아키텍처의 익셉션에 대해서 배워 봅시다.

그 동안 Arm 아키텍처는 CPU 아키텍처 시장에서 '저전력', '소형 디바이스' 용으로 사용되는 CPU 아키텍처로 분류됐습니다. 최대한 적은 트렌지스터를 사용해 최소의 소모 전력으로 CPU를 설계했기 때문입니다. 물론 Arm 코어의 CPU 아키텍처도 최대한 심플하게 디자인했습니다.

익셉션의 구조 관점으로 보면, Armv7 아키텍처의 익셉션도 다음과 같이 심플하게 설계됐다고 볼 수 있습니다. 

   ❑ 익셉션 벡터가 4바이트 단위로 정렬
   ❑ 어떤 Arm 동작 모드에서 익셉션이 발생해도 익셉션 벡터로 브랜치
   ❑ 익셉션의 종류도 8개로 정의 내림

그런데, Armv8 아키텍처는 CPU 아키텍처 시장에 "우리는 고성능 컴퓨터나 클라우드 서버에 진출하겠다"라고 출사표를 던지고 설계한 것으로 보입니다. 기존의 소형 저전력 디바이스 용으로 설계된 CPU 아키텍처가 아니라 인텔의 x86과 경쟁할 수 있는 CPU 아키텍처를 설계한 것입니다.  

이런 면모는 Armv8 아키텍처의 익셉션에서 볼 수 있습니다. 이제 Armv8 아키텍처의 익셉션의 주요 특징을 Armv7 익셉션과 비교하면서 알아보겠습니다.

ARM 아키텍처에서는 메모리 어보트 타입과 더불어 외부 인터럽트도 익셉션의 한 종류로 처리합니다. 외부 인터럽트는 메모리 어보트와 달리 하나의 기능으로 동작하므로, 외부 인터럽트의 익셉션 핸들러는 인터럽트 서비스 루틴을 실행합니다. 인터럽트 서비스 루틴을 통해 외부 하드웨어의 변화에 대해 소프트웨어적인 처리를 수행합니다.

그런데 인터럽트 타입 익셉션은 메모리 어보트 타입 익셉션과 어떤 차이점이 있을까요? 메모리 어보트 타입 익셉션은 ARM 코어가 명령어를 제대로 실행할 수 없는 상황에서 유발됩니다. 하지만, 인터럽트 타입 익셉션은 메모리 어보트 타입 익셉션과 달리 외부 하드웨어가 인터럽트라는 전기 신호를 발생시키면 ARM 코어가 이를 익셉션의 한 종류로 받아 처리하는 것입니다. 즉, 외부 하드웨어에서 어떤 변화를 알리기 위해 인터럽트라는 전기 신호를 발생하면 이를 소프트웨어적으로 처리하기 위한 인터페이스이자 운영체제 커널의 기능 중 하나인 것입니다.

인터럽트 타입 익셉션에 대해 소개했으니 다음 그림을 보면서 인터럽트 타입 익셉션의 전체 실행 흐름을 알아봅시다. 

 


그림 8.3 인터럽트 타입 익셉션의 전체 실행 흐름

익셉션의 전체 실행 흐름도는 5단계로 분류할 수 있는데, 각 단계 별로 어떤 동작을 수행하는지 알아봅시다. 

 


1단계: 프로세스가 실행하는 도중에 외부 인터럽트 발생 

 

먼저 ①로 표시된 부분을 따라가 봅시다. 프로세스가 실행하는 도중에 외부 하드웨어에서 인터럽트가 발생하는 부분을 나타냅니다. 소프트웨어 관점으로 보면, 프로세스가 실행 중에 인터럽트가 발생하면 프로세스의 실행 흐름이 멈추게 되는 것입니다.

 

2단계: ARM 프로세서가 익셉션을 감지 

 

② 로 표시된 부분은 ARM 프로세서가 인터럽트 타입 익셉션을 감지하는 동작입니다. 1단계에서 외부 하드웨어에서 인터럽트가 발생하면 ARM 코어는 다음과 같은 익셉션을 유발합니다.  

    ❑ IRQ interrupt
    ❑ FIQ interrupt

②~③으로 표기된 부분의 외곽에 보이는 테두리는 ARM 프로세서가 하드웨어적으로 처리하는 동작을 나타냅니다. 

 


3단계: ARM 프로세서가 익셉션에 대한 세부 처리 

 

③으로 표기된 박스를 보겠습니다. 익셉션을 감지한 ARM 프로세서(ARMv7 기준)는 다음과 같은 동작을 처리합니다.

    ❑ 익셉션이 발생한 모드를 나타내는 spsr_irq 레지스터에 저장한다. 
    ❑ ARM의 동작 모드를 IRQ 모드로 변경한다.
    ❑ 익셉션 벡터 베이스 주소를 먼저 찾은 다음에 0x18 주소를 더한 값을 프로그램 카운터로 브랜치한다.

ARM 코어는 외부 인터럽트를 감지하면 "인터럽트 익셉션을 유발해야 겠다"라고 판단하는데, 위와 같은 순서로 레지스터를 설정합니다. 3단계까지의 동작은 "하드웨어적으로" ARM 프로세서가 처리합니다.  


4단계: 익셉션 핸들러에서 인터럽트 서비스 루틴 실행 

 

3단계에서 인터럽트 익셉션을 유발하면 익셉션 벡터 베이스 주소 기준으로 0x18 주소 오프셋을 적용해 프로그램 카운터를 브랜치합니다. 즉, 익셉션 벡터 주소로 프로그램 카운터를 넣어주게 되는데, 익셉션 벡터 베이스 주소 기준으로 0x18 주소 오프셋에 위치한 명령어가 실행됩니다. 이 부분이 소프트웨어적으로 익셉션 핸들러 코드가 실행되는 부분인데, ④으로 표기된 박스에 해당됩니다.

 IRQ 익셉션 핸들러는 다음과 같은 동작을 수행합니다.

    ❑ 인터럽트가 발생한 시점의 레지스터 세트를 프로세스의 스택 공간에 푸시
    ❑ 인터럽트 서비스 루틴을 실행해 인터럽트를 핸들링하는 코드를 실행
    ❑ 인터럽트 핸들러 함수가 호출됨

IRQ 익셉션은 메모리 어보트 타입 익셉션과는 달리 운영체제에서 인터럽트를 처리하는 기능으로 동작합니다. 이 점이 메모리 어보트 타입 익셉션과는 다르다라는 점을 기억합시다.


5단계: 인터럽트 익셉션이 발생한 주소로 복귀 

 

인터럽트 서비스 루틴에서 인터럽트에 대한 처리를 마무리한 다음에 인터럽트가 발생할 시점의 명령어가 실행된 주소로 복귀하는 동작을 수행합니다. 인터럽트 익셉션이 발생했을 시점에 프로세스의 스택 공간에 푸시된 레지스터 세트를 ARM 코어의 레지스터에 다시 로딩하는 것입니다. 

여기까지 ARM 아키텍처 관점으로 인터럽트 벡터 핸들러가 수행하는 동작을 설명했습니다. 인터럽트 익셉션의 전체적인 실행 흐름에 대해 정확히 이해하려면, 인터럽트 익셉션을 운영체제의 관점으로 분석할 필요가 있습니다. 그러면 운영체제 관점으로 인터럽트 익셉션은 어떻게 볼 수 있을까요? 인터럽트 익셉션은 운영 체제에서 하나의 기능으로 동작합니다. 프로세스 관점으로 보면 프로세스가 실행하는 도중에 인터럽트 익셉션이 발생한 것으로 볼 수 있습니다. 따라서 인터럽트 익셉션이 발생한 후 처리되는 인터럽트 서비스 루틴의 코드는 빠르고 간결하게 처리되어야 합니다.

+ Recent posts