대부분시스템소프트웨어개발자는 C 언어로프로그래밍합니다. C 언어로작성된코드는컴파일러에의해기계어로변환되는데, 이과정에서컴파일러는성능을최적화하기위해코드를나름대로재해석합니다. 컴파일러에의해최적화된코드를보면 "참컴파일러는똑똑하구나, 어떻게코드를알아서생성했을까!"란생각이듭니다.
C 코드를컴파일할때컴파일러최적화옵션을적용하는데, 최적화레벨이높을수록컴파일러는더높은수준의최적화를수행합니다. 하지만컴파일러는프로그래머가작성한코드를최적화하는과정에서종종사고를치기도합니다. 코드를최적화하다보니선을넘어프로그래머가의도하지않은코드를생성하기때문입니다. 예를들어, 함수를인라인으로처리하면서심벌이제거되기도합니다.
컴파일러가코드를생성하는과정에서최적화를수행해문제를일으키면차라리낫습니다. C 코드와컴파일러가생성한어셈블리명령어를비교하면컴파일러가명령어를어떻게생성했는지확인할수있기때문입니다. 하지만 Arm 프로세서내부에서명령어의순서를바꿔서실행하면소프트웨어개발자가이를눈으로확인할수없습니다. Arm 프로세서는명령어간의존성이없다면성능을최적화하기위해 '메모리액세스' 순서를변경할수있기때문입니다.
지금까지 Arm 아키텍처에서 정의된 노멀 메모리 타입과 디바이스 메모리 타입을 알아봤습니다. 이어서 메모리 맵을 보면서 노멀 메모리 타입과 디바이스 메모리 타입의 특징을 자세히 알아봅시다.
[정보] 메모리 맵이란?
실전 프로젝트를 진행하면 메모리 맵이란 용어를 자주 듣습니다. 메모리 맵은 무엇일까요? 프로그램에 의해 실행되는 프로세스 입장에서 바라 본 메모리 레이아웃입니다. 지도를 보면 일정한 규칙(시/도)에 따라 구획이 나눠져 있듯이 메모리 영역을 속성별로 분류한 일종의 메모리 지도가 메모리 맵입니다.
메모리 맵의 구조도 알아보기
메모리 맵은 데이터 영역, 코드와 같은 속성으로 마킹된 다양한 영역으로 구성돼 있습니다. 다음 그림을 보면서 메모리 맵에 대해 더 자세히 알아봅시다.
그림 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 아키텍처 관점에서 보면 메모리 타입은 크게 두 가지로 분류됩니다.
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를 보내고 페리페럴의 인터럽트를 비활성화하는 명령어를 작성했는데, 명령어의 순서가 바뀌면 오동작할 수 있습니다. 즉, 디바이스 메모리 타입으로 분류되는 영역이 노멀 메모리 타입처럼 처리되면 이처럼 예상치 못한 사이트 이펙트가 유발될 수도 있습니다.
CPU 입장에서는레지스터보다메모리에접근할때더많은사이클과리소스를사용합니다. 그이유는무엇일까요? 메모리에접근하려면메모리버스에먼저접근해야하며, DRAM 메모리콘트롤러와같은메모리하드웨어에서부가적인동작을수행하기때문입니다. 메모리에액세스하는횟수가늘어날수록이에비례해더많은사이클을소모합니다. 따라서메모리에접근하는횟수를줄이면시스템성능을더높일수있습니다.
Arm 프로세서내부에도메모리에액세스하는횟수를최소화해성능을높이는메커니즘이적용돼있습니다.예를들어, 같은주소공간에있는메모리에두번액세스하는명령어가있으면이를합쳐서한번만메모리에접근할수있습니다.
스페큘레이션액세스
성능최적화를위한다른 CPU 설계기법은스페큘레이션액세스입니다. 소프트웨어에서접근할것으로예상하는데이터를패턴인식과같은알고리즘을활용해미리예측해미리로딩하는동작을스페큘레이션액세스라고하며이를통해성능을높일수있습니다.
이밖에도 Arm 프로세서는내부에서분기예측, 실행예측을통해성능을극대화하는메커니즘이적용되어있습니다.
리오더링액세스
Arm 프로세서는성능을더향상하기위해프로세서내부에서명령어실행순서를바꿔서실행할수있습니다. 이를위해Arm 프로세서는명령어간에의존성이있는지체크하며, 의존성이없는경우명령어의순서를바꿔처리합니다.
Arm 프로세서는내부적으로성능을극대화하기위해다양한기법을적용해왔습니다. 이가운데대표적인기법은 "데이터처리량을높이기위해메모리읽기혹은쓰기작업을재정렬(reorder)하는동작"으로, 이를메모리리오더링이라고합니다. 메모리리오더링을통해프로세서와외부메모리간의데이터처리량을늘릴수있고, 그결과프로세서의성능을높일수있습니다.
많은 IT 업체들이해킹으로피해를보면서보안환경을구축할수있는보안시스템의필요성을절감하기에이르렀습니다. 그래서 IT업체들은중요한데이터를암호화하거나해커의공격으로부터보호할수있는시스템을설계합니다. 고육지책으로보안과관련된기능을제공하는하드웨어부품을시스템에탑재합니다. 이같은노력으로중요한데이터는보호할수있으나회로설계나소프트웨어의복잡도가늘어나고, 그결과로제품의가격이높아집니다.
결국 CPU 아키텍처에서보안과관련된기능(데이터암호화, 복호화)을제공해처음부터보안을고려해시스템을설계할수있는환경이구축되면좋겠다는요구사항이생겼습니다. 특히SoC 벤더나보안업체가효율적으로보안환경을구축할수있는아키텍처가필요했습니다. 또한소프트웨어가공격당할수있는취약점이너무다양한데, 이를어떻게방어할지에대해고민했습니다.
많은 분들이 생각하는 익셉션의 개념은 'Armv7 아키텍처의 익셉션'인 경우가 많아, Armv8 아키텍처를 배울 때 Armv7 아키텍처의 익셉션과 비슷할 것이라 예상합니다. 익셉션이 발생하면 지정한 주소로 프로그램 카운터를 브랜치하는 기본 개념은 Armv7/Armv8 아키텍처가 같으나, 익셉션을 처리하고 분류하는 체계와 세세한 처리 방식이 많이 다릅니다.
Armv8 아키텍처의 익셉션은 고성능 컴퓨터에서 적용되는 트러스트 존이나 가상화 시스템인 하이퍼바이저를 이해하기 위해 반드시 알아야 할 기반 지식이므로, 반드시 잘 알아야 둬야 합니다.
익셉션(Exception)은 Armv8 아키텍처의 핵심 기능 중 하나입니다. Armv8 아키텍처에서 정의된 익셉션를 활용해 하이퍼바이저와 같은 가상 시스템을 설계할 수 있습니다. 또한 트러스트 존을 제대로 이해하려면 먼저 익셉션의 동작 방식을 알아야 합니다.
Armv8 아키텍처에서 익셉션은 어떻게 정의내릴 수 있을까요? 기존에 소개된 Armv8 아키텍처의 익셉션과는 어떤 차이점이 있을까요? Armv8 아키텍처에서는 기존 Armv7 아키텍처와 비교해 익셉션의 종류를 분류하는 방식이 조금 다르지만, 익셉션의 동작 원리는 동일합니다. Armv8 아키텍처의 익셉션은 다음과 같이 설명할 수 있습니다.
“익셉션이란 Arm 코어가 명령어를 처리하다가 예외 사항이 발생할 때 이를 처리하는 방식이다. 익셉션이 발생하면 익셉션 벡터로 프로그램 카운터가 브랜치된다.”
위 문장을 읽으면 "어, Armv8 아키텍처의 익셉션은 Armv7 익셉션과 거의 같네"라는 생각이 들수 있습니다. 하지만 Armv8 아키텍처의 익셉션은 Armv7 익셉션과 비교했을 때 기본 개념은 같지만 익셉션을 분류하고 처리하는 방식이 다릅니다.
그 동안 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 아키텍처 관점으로 인터럽트 벡터 핸들러가 수행하는 동작을 설명했습니다. 인터럽트 익셉션의 전체적인 실행 흐름에 대해 정확히 이해하려면, 인터럽트 익셉션을 운영체제의 관점으로 분석할 필요가 있습니다. 그러면 운영체제 관점으로 인터럽트 익셉션은 어떻게 볼 수 있을까요? 인터럽트 익셉션은 운영 체제에서 하나의 기능으로 동작합니다. 프로세스 관점으로 보면 프로세스가 실행하는 도중에 인터럽트 익셉션이 발생한 것으로 볼 수 있습니다. 따라서 인터럽트 익셉션이 발생한 후 처리되는 인터럽트 서비스 루틴의 코드는 빠르고 간결하게 처리되어야 합니다.
Armv7 아키텍처의 익셉션과 비교했을 때, Armv8 아키텍처에서는 익셉션을 분류하는 체계와 익셉션 벡터 테이블이 약간 다릅니다. 그 특징에 대해서 더 자세히 알아봅시다.
첫째, Armv8 아키텍처에서는 익셉션의 종류를 계층 구조로 재정의했습니다. 먼저 익셉션을 Synchronous와 Asynchronous와 같이 큰 카테고리로 분류하고, 하부 카테고리로 익셉션 클래스를 정의했습니다. Armv7 의 익셉션의 종류는 익셉션 클래스에서 확인할 수 있습니다.
표 9.1 Armv8 아키텍처에서 익셉션의 분류 체계
먼저 익셉션을 Synchronous와 Asynchronous와 같이 큰 카테고리로 분류합니다. Arm 코어가 명령어를 실행하다가 유발하는 익셉션을 Synchronous, 외부 인터럽트나 외부 메모리 어보트와 같이 외부에서 비동기적으로 유발되는 익셉션을 Asynchronous로 분류합니다. 이처럼 기존 Armv7 아키텍처의 익셉션과 비교해 익셉션을 분류하는 방식이 다릅니다.
둘째, 익셉션 레벨(EL)이란 개념을 도입해 이 기준으로 익셉션을 처리합니다. 익셉션이 유발된 익셉션 레벨 별로 익셉션 벡터 주소가 존재하며, 익셉션 레벨 별로 익셉션 링크 레지스터를 정의합니다.
다음 그림은 Armv8 아키텍처에서 익셉션 레벨 별로 익셉션 처리되는 구조를 나타냅니다.
그림 9.1 Armv8 아키텍처에서 익셉션 레벨 별로 익셉션이 처리되는 흐름
그림의 윗 부분을 먼저 보겠습니다. 유저 애플리케이션이 구동되는 EL0에서 익셉션이 발생하면 다음 순서로 처리됩니다.
이처럼 Arm코어는 익셉션이 유발되면 익셉션이 발생한 익셉션 레벨 별로 지정된 익셉션 벡터로 프로그램 카운터를 브랜치합니다.
[정보] EL0과 EL1은 각각 Armv7 아키텍처의 User 모드, Supervisor 모드에 대응됩니다. Armv7 아키텍처의 User 모드에서는 유저 애플리케이션이 구동하고, Supervisor 모드에서는 운영체제의 커널이 동작합니다. 마찬가지로, Armv8 아키텍처의 EL0에서는 유저 애플리케이션이 실행되고, EL1에서는 운영체제의 커널이 동작합니다.
Armv7 아키텍처에서는 어떤 Arm 동작 모드에서 익셉션이 유발돼도 익셉션 종류 별로 지정된 익셉션 벡터로 프로그램 카운터가 브랜치됩니다. 예를 들어 유저 애플리케이션이 실행되는 User 모드나 운영체제의 커널이 구동되는 슈퍼바이저 모드에서 데이터 어보트가 유발되면, 데이터 어보트 익셉션에 해당되는 익셉션 벡터로 프로그램 카운터가 브랜치됩니다.
그래서 익셉션 핸들러에서 익셉션이 유발된 시점의 Arm 동작 모드를 읽어서 이를 세분화하는 명령어를 입력해야 합니다. 예를 들어, 리눅스 커널인 경우 다음 코드와 같이 Arm 동작 모드를 읽어서 세분화해 처리하는 동작을 확인할 수 있습니다.
다음은 Armv7 아키텍처 기반 리눅스 커널에서 구현된 Undefined Instruction 익셉션 핸들러의 구현부입니다.