1. 0xc13342c0 가상 주소가 물리주소로 변환되는 과정 확인

crash> p schedule 
schedule = $1 =
 {int (void)} 0xc13342c0 <schedule>




물리주소는 0x813342c0임

crash> vtop 0xc13342c0
VIRTUAL   PHYSICAL
c13342c0  813342c0

PAGE DIRECTORY: c0004000
  PGD: c000704c => 8131940e
  PMD: c000704c => 8131940e
 PAGE: 81300000  (1MB)


  PAGE    PHYSICAL   MAPPING    INDEX CNT FLAGS
c4926680  81334000         0         0  1 400 reserved


0xc13342c0 가상 주소에서 L1 Page Table Index는 c13임(0xc13342c0)

crash> p schedule
schedule = $1 =
 {int (void)} 0xc13342c0 <schedule>



TTBA(Translation Table Base Address)는 0x80004000임


0x80004000-- 0x80006FB0까지 0으로 채워짐


0xc13342c0 가상 주소에 대한 PageTable Dump를 보면 아래와 같음



L1 Page Table은 0x8000704C = 0x80004000 + (0xC13 * 4 = 0x304C)

0x8131940E(*0x8000704C) = 10000001001100011001010000001110

 


10000001001100011001010000001110 값에서 마지막 2비트가 10이므로 Section Entry임

앞의 12비트가 Base Address임 10000001001100011001010000001110, 0x813 = 100000010011


0x813(Physical Base Address) 에서 20-Bit Left Shift + 가상 주소의 Offset 값(0xc13342c0) = 0x813342c0

 

공식: 0x813(PA 변환된 정보) + 342c0 오프셋 =  0x813342c0


Page Descriptor 주소는 c4926680

crash> vtop 0xc13342c0
VIRTUAL   PHYSICAL
c13342c0  813342c0

PAGE DIRECTORY: c0004000
  PGD: c000704c => 8131940e
  PMD: c000704c => 8131940e
 PAGE: 81300000  (1MB)


  PAGE    PHYSICAL   MAPPING    INDEX CNT FLAGS
c4926680  81334000         0         0  1 400 reserved


0xc13342c0 주소에 해당하는 페이지 프레임 번호는 0x1334임

(struct page *) &mem_map[0x0] = 0xC4900000
  (struct page *) (struct page*)0xC4926680
 
  0xC4926680 - 0xC4900000 = 0x26680 / 0x20 = 0x1334

 



2. 0xBFED1000 가상 주소가 물리주소로 변환되는 과정 확인

물리주소는 0xc893a000임

 

crash> vtop BFED1000
VIRTUAL   PHYSICAL
bfed1000  c893a000

PAGE DIRECTORY: c0004000
  PGD: c0006ff8 => a70e6811
  PMD: c0006ff8 => a70e6811
  PTE: a70e6344 => c893a65f

 PAGE: c893a000

  PTE     PHYSICAL  FLAGS
c893a65f  c893a000  (PRESENT|DIRTY|YOUNG|WRITE)

  PAGE    PHYSICAL   MAPPING    INDEX CNT FLAGS
c5212740  c893a000  e1367001        97  1 40080068 uptodate,lru,active,swapbacked

 

아래 과정으로 L1 Page Table Entry에 접근

L1 Page Table entry: 0xBFE(0xBFED1000)
 

Page Table Address 0x80006FF8 = 0x80004000 + 0x2FF8(0xBFE*4)
A70E6811=*0x80006FF8

 

// 2bit가 01이니까 다음 레벨의 페이지 테이블의 베이스 주소를 가리킴 

10100111000011100110100000010001(0xA70E6811) 


10100111000011100110100000010001 [31:10] 비트만 살리고 나머지는 0으로 밀어버리면
10100111000011100110100000000000 --> A70E6800 (Next, 2 level 페이지 테이블)

 

0xA70E6B44 = 0xA70E6800 + 0x344 [where: 4*0xD1(0xBFED1000) ]

 

<관련 Arm 스펙 문서>

 

이어서 2 레벨 페이지 변환을 진행하자.

0xA70E6B44 = 0xA70E6800 + 0x344 [where: 4*0xD1(0xBFED1000) ]

 

PTE인 C893A45F을 2진수로 표기하면 아래와 같다.

1100 1000 1001 0011 1010 0100 0101 1111

bit[1:0]이 11이니 스몰 페이지(Small page)이다.

 

<스몰 페이지에 대한 Arm 스펙 문서는 아래와 같다.>


결국 아래와 같이 계산하여 물리 주소를 얻음

C893A45F(2 level PTE 값)
BFED1000(가상 주소의 offset)
C893A000 (C893A45F)

다른 주소로 테스트

crash> vtop BFED1111
VIRTUAL   PHYSICAL
bfed1111  c893a111

crash> vtop BFED1ddd
VIRTUAL   PHYSICAL
bfed1ddd  c893addd


T32로 다시 확인



User space 가상 주소에 대한 Page Address

crash> vtop BFED1000
VIRTUAL   PHYSICAL
bfed1000  c893a000

PAGE DIRECTORY: c0004000
  PGD: c0006ff8 => a70e6811
  PMD: c0006ff8 => a70e6811
  PTE: a70e6344 => c893a65f

 PAGE: c893a000

  PTE     PHYSICAL  FLAGS
c893a65f  c893a000  (PRESENT|DIRTY|YOUNG|WRITE)

  PAGE    PHYSICAL   MAPPING    INDEX CNT FLAGS
c5212740  c893a000  e1367001        97  1 40080068 uptodate,lru,active,swapbacked


Offset 계산
 

 (struct page_address_map *) &page_address_maps[0x0] = 0xC15F71C0 = page_address_maps[0] -> (
________address|________0________4________8________C_0123456789ABCDEF
   NSD:C15F7EB0| C557BBC0 BFECF000 C15F8478 C15F7C98 ..W.....x._..|_.
   NSD:C15F7EC0| C570CB80 BFED0000 C15F84F8 C15F5940 ..p......._.@Y_.
   NSD:C15F7ED0| C5212740 BFED1000 C15F85D8 C15F6000 @'!......._..`_.
   NSD:C15F7EE0| C54A4BC0 BFED2000 C15F7F58 C15F5B00 .KJ.. ..X._..[_.
   0xD10 = 0xC15F7ED0 - 0xC15F71C0

 



page_address_maps 전역변수는 0xBFE00000 - 0xBFFFF000 구간 가상 주소에 대한 페이지 정보를 포함

 

이번 포스트에서는 RISC-V 툴체인을 설치해 리눅스 커널을 빌드하는 방법을 소개합니다.

먼저 RISC-V 툴체인을 설치하는 명령어를 입력합시다.

RISC-V 툴체인 소스를 내려받기  

다음 명령어를 입력해 RISC-V 툴체인 소스를 내려받습니다.

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
 
아래는 리눅스 터미널에서 위 명령어를 입력한 후의 출력 결과입니다. 소스를 내려받는데 1시간 정도 걸리네요.

austindh.kim:~/src/risc-v_toolchain$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
Cloning into 'riscv-gnu-toolchain'...
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 7795 (delta 7), reused 16 (delta 4), pack-reused 7774
Receiving objects: 100% (7795/7795), 4.75 MiB | 970.00 KiB/s, done.
Resolving deltas: 100% (3940/3940), done.
Submodule 'qemu' (https://git.qemu.org/git/qemu.git) registered for path 'qemu'
Submodule 'riscv-binutils' (https://github.com/riscv/riscv-binutils-gdb.git) registered for path 'riscv-binutils'
Submodule 'riscv-dejagnu' (https://github.com/riscv/riscv-dejagnu.git) registered for path 'riscv-dejagnu'
Submodule 'riscv-gcc' (https://github.com/riscv/riscv-gcc.git) registered for path 'riscv-gcc'
Submodule 'riscv-gdb' (https://github.com/riscv/riscv-binutils-gdb.git) registered for path 'riscv-gdb'
...

시간이 오래 걸리니 밥 먹기 전에 명령어를 입력해 놔야 겠습니다.

RISC-V 툴체인 소스 빌드하고 설치하기 

소스를 전부 내려받은 다음에 아래와 같이 디렉터리를 생성합니다.

$ mkdir -p opt/riscv
$ mkdir build
$ cd build
$ ../configure --prefix=/home001/austindh.kim/src/risc-v_toolchain/riscv-gnu-toolchain/opt/riscv --enable-multilib

혹은

$ ../configure --prefix=/home001/austindh.kim/src/risc-v_toolchain/riscv-gnu-toolchain/opt/riscv  



아래는 리눅스 터미널에서 위 명령어를 실행한 후 출력 결과입니다. 빌드되는데 2시간 정도가 소요됩니다.

austindh.kim:~/src/risc-v_toolchain$ mkdir -p opt/riscv
austindh.kim:~/src/risc-v_toolchain$ mkdir build
austindh.kim:~/src/risc-v_toolchain$ cd build
austindh.kim:~/src/risc-v_toolchain/build$ ../configure --prefix=/home001/austindh.kim/src/risc-v_toolchain/riscv-gnu-toolchain/opt/riscv 
...

 

이번에는 다음 명령어를 실행해서 빌드를 수행합니다.

 

austindh.kim:~/src/risc-v_toolchain/build$ make linux


빌드가 끝나고 "~src/risc-v_toolchain/riscv-gnu-toolchain/opt/riscv/bin" 디렉터리로 이동하니, 다음과 같은 파일이 생성됐음을 확인할 수 있습니다.

austindh.kim:~/src/risc-v_toolchain/riscv-gnu-toolchain/opt/riscv/bin$ ls
riscv64-unknown-linux-gnu-addr2line  riscv64-unknown-linux-gnu-gcc         riscv64-unknown-linux-gnu-gdb            riscv64-unknown-linux-gnu-objcopy  ... 
riscv64-unknown-linux-gnu-elfedit    riscv64-unknown-linux-gnu-gcov-dump   riscv64-unknown-linux-gnu-lto-dump       riscv64-unknown-linux-gnu-strings
riscv64-unknown-linux-gnu-g++        riscv64-unknown-linux-gnu-gcov-tool   riscv64-unknown-linux-gnu-nm             riscv64-unknown-linux-gnu-strip

이제 RISC-V 툴체인 소스 빌드를 마무리했습니다.

 RISC-V 툴체인으로 리눅스 커널 빌드하기 

다음 명령어를 입력해 리눅스 커널 소스 코드를 내려 받습니다.

$ git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/next/linux-next

소스를 내려 받은 다음에 확인한 디렉터리 정보는 다음과 같습니다.

austindh.kim:~/src/dev_kernel/59_linux_kernel$ ls
linux-next

'riscv_kernel_build.sh' 이름으로 셸 스크립트 파일을 생성한 다음에, 아래 빌드 스크립트를 입력합니다.

 

#!/bin/bash

export PATH=$PATH:/home001/austindh.kim/src/risc-v_toolchain/riscv-gnu-toolchain/opt/riscv/bin

echo "configure build output path"
TOP_PATH=$( cd "$(dirname "$0")" ; pwd )
OUTPUT="$TOP_PATH/out"

BUILD_LOG="$TOP_PATH/rpi_build_log.txt"

OUTPUT_PATH=$( cd "$(dirname "$0")" ; pwd )
OUTPUT="$OUTPUT_PATH/out"

pushd linux-next > /dev/null

# configure defconfig
make ARCH=riscv O=$OUTPUT CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig -j16  2>&1

# build kernel with riscv tool-chain
make ARCH=riscv O=$OUTPUT CROSS_COMPILE=riscv64-unknown-linux-gnu- modules dtbs -j16  2>&1 | tee $BUILD_LOG

popd > /dev/null


위 빌드 스크립트에서 가장 중요한 명령어는 아래와 같습니다.


# configure defconfig
make ARCH=riscv O=$OUTPUT CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig -j16  2>&1

# build kernel with riscv tool-chain
make ARCH=riscv O=$OUTPUT CROSS_COMPILE=riscv64-unknown-linux-gnu- modules dtbs -j16  2>&1 | tee $BUILD_LOG



'arch/riscv/configs/defconfig' 파일을 다음과 같이 수정해 vmlinux에 디버깅 심벌이 추가되도록 합시다.

diff --git a/arch/riscv/configs/defconfig b/arch/riscv/configs/defconfig
index d58c93e..6ea8e83 100644
--- a/arch/riscv/configs/defconfig
+++ b/arch/riscv/configs/defconfig
@@ -20,6 +20,7 @@ CONFIG_SMP=y
 CONFIG_JUMP_LABEL=y
 CONFIG_MODULES=y
 CONFIG_MODULE_UNLOAD=y
+CONFIG_DEBUG_INFO=y
 CONFIG_NET=y
 CONFIG_PACKET=y
 CONFIG_UNIX=y


'riscv_kernel_build.sh' 빌드 셸 스크립트를 실행해, RISCV 툴 체인으로 커널을 빌드합니다.

아래는 'riscv_kernel_build.sh' 빌드 셸 스크립트를 터미널에서 실행한 출력 화면입니다.
austindh.kim:~/src/dev_kernel/59_linux_kernel$ ./riscv_kernel_build.sh
configure build output path
make[1]: Entering directory '/home001/austindh.kim/src/dev_kernel/59_linux_kernel/out'
  GEN     Makefile
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
...
  KSYMS   .tmp_vmlinux.kallsyms2.S
  AS      .tmp_vmlinux.kallsyms2.S
  LD      vmlinux
  SYSMAP  System.map
  MODPOST Module.symvers
  CC [M]  fs/nfs/flexfilelayout/nfs_layout_flexfiles.mod.o
  LD [M]  fs/nfs/flexfilelayout/nfs_layout_flexfiles.ko


'out' 폴더를 가니 제대로 커널이 빌드됐음을 확인할 수 있습니다.

austindh.kim:~/src/dev_kernel/59_linux_kernel/out$ ls
arch   crypto   include  kernel    mm                       modules.order   scripts   source      virt       vmlinux.symvers
block  drivers  init     lib       modules.builtin          Module.symvers  security  System.map  vmlinux
certs  fs       ipc      Makefile  modules.builtin.modinfo  net             sound     usr         vmlinux.o


RISC-V 바이너리 유틸리티 사용해보기 

이번엔 다음 명령어를 입력해 vmlinux의 셉션 정보를 확인해보겠습니다.

  
$ ./riscv64-unknown-linux-gnu-objdump -x vmlinux | more


아래는 './riscv64-unknown-linux-gnu-objdump -x vmlinux | more' 빌드 셸 스크립트를 터미널에서 실행한 출력 화면입니다.

austindh.kim:~/src/dev_kernel/59_linux_kernel/out$ ./riscv64-unknown-linux-gnu-objdump -x vmlinux | more

vmlinux:     file format elf64-littleriscv
vmlinux
architecture: riscv:rv64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0xffffffe000000000

Program Header:
    LOAD off    0x0000000000001000 vaddr 0xffffffe000000000 paddr 0x0000000000000000 align 2**12
         filesz 0x0000000000025af4 memsz 0x0000000000025af4 flags r-x
    LOAD off    0x0000000000027000 vaddr 0xffffffe000026000 paddr 0x0000000000026000 align 2**12
         filesz 0x0000000000019de8 memsz 0x0000000000019de8 flags rwx
    LOAD off    0x0000000000041000 vaddr 0xffffffe000200000 paddr 0x0000000000200000 align 2**12
         filesz 0x00000000006ce0a2 memsz 0x00000000006ce0a2 flags r-x
    LOAD off    0x0000000000710000 vaddr 0xffffffe000a00000 paddr 0x0000000000a00000 align 2**12
         filesz 0x000000000022a9fc memsz 0x000000000022a9fc flags rw-
    LOAD off    0x000000000093b000 vaddr 0xffffffe000e00000 paddr 0x0000000000e00000 align 2**12
         filesz 0x0000000000002400 memsz 0x0000000000002400 flags r--
    LOAD off    0x000000000093e000 vaddr 0xffffffe001000000 paddr 0x0000000001000000 align 2**12
         filesz 0x00000000000e19b4 memsz 0x0000000000131920 flags rw-
    NOTE off    0x000000000093a9c0 vaddr 0xffffffe000c2a9c0 paddr 0x0000000000c2a9c0 align 2**2
         filesz 0x000000000000003c memsz 0x000000000000003c flags r--
   STACK off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
         filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-

  
출력 결과를 보니 아키텍처의 정보는 'riscv:rv64'이고 스타트업 코드의 위치는 0xffffffe000000000 주소임을 알 수 있습니다.

제가 인프런에 '시스템 소프트웨어 개발의 모든 것 - 시스템 반도체와 전기 자동차 중심' 강좌를 만들어서 오픈했습니다. 링크는 아래와 같습니다;

 

시스템 소프트웨어 개발의 모든 것 강의 링크

그 동안 여러 세미나에서 시스템 반도체와 전기 자동차 분야에서 시스템 소프트웨어 개발을 하고 싶은데 어떻게 무엇을 준비해야 할지 모르겠다는 질문을 많이 받았습니다. 제가 세미나에서 답을 드린 내용을 잘 정리하고 압축해 강의를 만들었습니다.

강의 내용을 압축하면 다음과 같습니다;

 1. 시스템 소프트웨어 개발자로 실전 프로젝트에서 어떤 일을 하는지 세세히 설명을 합니다. 
 2. 시스템 반도체와 전기 자동차 업계에서 리눅스 시스템 소프트웨어 개발자가 어떤 일을 하는지 자세히 다룹니다. 
 3. 각각 어떤 소프트웨어 스택으로 시스템 소프트웨어 관련 구조가 구성됐는지도 상세히 설명합니다.

여러분 중에 시스템 소프트웨어 분야를 진출하기를 희망 하시거나 혹은 실전 프로젝트에서 어떤 일을 하는지 궁금하신다면 이 강의를 참고하시면 좋겠습니다.

 

 

 

먼저 이번 절에서 분석할 XEN 하이퍼바이저에서 구현된 익셉션 핸들러의 전체 코드를 소개합니다. 

01  0x26a800 <hyp_traps_vector>:
02  0x26a800:    b    0x269800 <hyp_sync_invalid>
03  0x26a804:    nop
04  0x26a808:    nop
...
05  0x26a880:    b    0x26986c <hyp_irq_invalid>
06  0x26a884:    nop
07  0x26a888:    nop
...
08  0x26a900:    b    0x2698d8 <hyp_fiq_invalid>
09  0x26a904:    nop
10  0x26a908:    nop
...
11  0x26a980:    b    0x269944 <hyp_error_invalid>
12  0x26a984:    nop
13  0x26a988:    nop
...
14  0x26aa00:    b    0x269a1c <hyp_sync>
15  0x26aa04:    nop
16  0x26aa08:    nop
...
17  0x26aa80:    b    0x269a94 <hyp_irq>
18  0x26aa84:    nop
19  0x26aa88:    nop
...
20  0x26ab00:    b    0x2698d8 <hyp_fiq_invalid>
21  0x26ab04:    nop
22  0x26ab08:    nop
...
23  0x26ab80:    b    0x2699b0 <hyp_error>
24  0x26ab84:    nop
25  0x26ab88:    nop
...
26  0x26ac00:    b    0x0x269b14 <guest_sync>
27  0x26ac04:    nop
28  0x26ac08:    nop
...
29  0x26ac80:    b    0x269c78 <guest_irq>
30  0x26ac84:    nop
31  0x26ac88:    nop
...
32  0x26ad00:    b    0x269d4c <guest_fiq_invalid>
33  0x26ad04:    nop
34  0x26ad08:    nop
...
35  0x26ad80:    b    0x269de0 <guest_error>
36  0x26ad84:    nop
37  0x26ad88:    nop
...
38  0x26ae00:    b    0x269eb4 <guest_sync_compat>
39  0x26ae04:    nop
40  0x26ae08:    nop
...
41  0x26ae80:    b    0x269f90 <guest_irq_compat>
42  0x26ae84:    nop
43  0x26ae88:    nop
...
44  0x26af00:    b    0x26a06c <guest_fiq_invalid_compat>
45  0x26af04:    nop
46  0x26af08:    nop
...
47 0x26af80:    b    0x26a104 <guest_error_compat>

익셉션 핸들러의 코드를 분석하기 전에 "'익셉션 벡터 베이스 주소 + 오프셋 주소' 규칙으로 프로그램 카운터가 분기된다"라는 사실을 염두에 둡시다. 또한 VBAR_EL2 레지스터는 익셉션 핸들러의 시작 주소를 저장한다는 사실을 기억합시다. 이번 절에서 소개한 익셉션 핸들러의 베이스 주소와 오프셋 정보는 다음과 같습니다.

 익셉션 핸들러 베이스 주소: 0x26a800(VBAR_EL2)
 익셉션별 오프셋 주소의 간격: +0x80

EL2 익셉션 핸들러의 구조 파악

이해를 돕기 위해 다음 예제 코드를 보면서 설명하겠습니다.

01  0x26a800 <hyp_traps_vector>:
02  0x26a800:    17fffc00     b    0x269800 <hyp_sync_invalid>
03  0x26a804:    d503201f     nop
04  0x26a808:    d503201f     nop
...
05  0x26a880:    17fffbfb     b    0x26986c <hyp_irq_invalid>
06  0x26a884:    d503201f     nop
07  0x26a888:    d503201f     nop

'EL2 with SP_EL0'와 같이 스택 포인터를 익셉션 레벨과 상관없이 설정하는 조건에서 유발되는 익셉션의 오프셋 정보는 다음과 같습니다.

 Synchronous 익셉션: 0x0
 IRQ 익셉션: 0x80

만약 'EL2 with SP_EL0' 조건에서 Synchronous 익셉션이 유발되면 다음과 같은 규칙에 따라 프로그램 카운터가 0x26a800 주소로 분기됩니다.

 0x26a800 = 0x26a800(VBAR_EL2) + 0x0(오프셋)

그런데 'EL2 with SP_EL0' 조건에서 'IRQ Interrupt' 익셉션이 유발되면 프로그램 카운터가 0x26a880 주소로 분기됩니다. 프로그램 카운터가 바뀌는 공식은 다음과 같습니다.

 0x26a880 = 0x26a800(익셉션 벡터 베이스 주소) + 0x80(오프셋)

이번에는 하이퍼바이저가 실행되는 EL2에서 익셉션이 유발되면 프로그램 카운터가 어떻게 분기되는지 알아보겠습니다.

14  0x26aa00:    17fffc07     b    0x269a1c <hyp_sync>
15  0x26aa04:    d503201f     nop
16  0x26aa08:    d503201f     nop
...
17  0x26aa80:    17fffc05     b    0x269a94 <hyp_irq>
18  0x26aa84:    d503201f     nop
19  0x26aa88:    d503201f     nop

EL2에서 발생하는 익셉션의 오프셋 정보는 다음과 같습니다.

 Synchronous 익셉션: 0x200
 IRQ 익셉션: 0x280

만약 EL2에서 Synchronous 익셉션이 유발되면 다음과 같은 공식에 따라 프로그램 카운터가 0x26aa00 주소로 분기됩니다.

 0x26aa00 = 0x26a800(VBAR_EL2) + 0x200(오프셋)

이와 마찬가지로 EL2에서 'IRQ Interrupt' 익셉션이 유발되면 프로그램 카운터가 0x26aa80 주소로 분기됩니다. 규칙은 다음과 같습니다.

 0x26aa80 = 0x26a800(VBAR_EL2) + 0x280(오프셋)

이처럼 XEN 하이퍼바이저의 익셉션 벡터 테이블은 Armv8 아키텍처에서 명시된 익셉션 벡터 테이블의 스펙에 따라 구현됐다는 점을 알 수 있습니다. 

XEN 하이퍼바이저 관점에서 익셉션 핸들러 구조 정리

다음은 앞에서 설명한 익셉션 핸들러 코드에서 익셉션 종류별로 분기되는 프로그램 카운터의 정보를 정리한 표입니다.

 

 

 

 

 

 

 

 

 

 

익셉션 핸들러의 코드를 제대로 이해하려면 Arm 아키텍처에서 정의된 익셉션 벡터 테이블을 먼저 숙지해야 합니다. 

익셉션 핸들러 코드 분석

XEN 하이퍼바이저의 익셉션 핸들러를 해석하는 방법과 전체 구조를 소개했으니 익셉션 핸들러 코드를 분석하겠습니다.

익셉션 핸들러의 앞부분에 있는 02 ~ 14번째 줄은 익셉션 레벨과 상관없이 같은 스택을 사용하도록 설정한 경우에 동작합니다. XEN 하이퍼바이저는 이 유형의 익셉션을 지원하지 않으므로 해당 익셉션 핸들러의 서브루틴에서는 디버깅 정보를 출력하고 크래시를 유발하는 루틴이 실행됩니다. 

EL2에서 발생한 익셉션에 대한 처리 루틴

이어서 14 ~ 25번째 줄을 분석하겠습니다.

14  0x26aa00:    17fffc07     b    0x269a1c <hyp_sync>
15  0x26aa04:    d503201f     nop
16  0x26aa08:    d503201f     nop
...
17  0x26aa80:    17fffc05     b    0x269a94 <hyp_irq>
18  0x26aa84:    d503201f     nop
19  0x26aa88:    d503201f     nop
...
20  0x26ab00:    17fffb76     b    0x2698d8 <hyp_fiq_invalid>
21  0x26ab04:    d503201f     nop
22  0x26ab08:    d503201f     nop
...
23  0x26ab80:    17fffb8c     b    0x2699b0 <hyp_error>
24  0x26ab84:    d503201f     nop
25  0x26ab88:    d503201f     nop

14 ~ 25번째 줄은 하이퍼바이저의 코드가 실행 중인 EL2에서 익셉션이 유발됐을때 처리되는 익셉션 핸들러입니다.

이번에는 익셉션 종류별로 처리되는 세부 코드를 분석하겠습니다. 먼저 14 ~ 16번째 줄을 봅시다.

14  0x26aa00:    17fffc07     b    0x269a1c <hyp_sync>
15  0x26aa04:    d503201f     nop
16  0x26aa08:    d503201f     nop

여기서 EL2의 Synchronous 익셉션은 어떻게 유발될까요? 이 질문에는 다음과 같이 답할 수 있습니다.

EL2에서 실행되는 하이퍼바이저에서 메모리 어보트를 유발하는 명령어가 실행된 경우에 Synchronous 익셉션이 유발된다.

Arm 아키텍처 관점에서 분석하면 EL2에서 Synchronous 익셉션이 유발될 때 14번째 줄에 보이는 0x26aa00 주소로 프로그램 카운터가 분기됩니다. 14번째 줄에 "b hyp_sync"라는 명령어가 있는데, 이 명령어가 실행되면 hyp_sync 레이블로 분기합니다.

hyp_sync 레이블의 서브루틴에서는 디버깅 정보를 출력하고 크래시를 유발하는 루틴이 처리됩니다. 

이어서 17 ~ 19번째 줄을 보겠습니다.

17  0x26aa80:    17fffc05     b    0x269a94 <hyp_irq>
18  0x26aa84:    d503201f     nop
19  0x26aa88:    d503201f     nop

EL2에서 실행되는 하이퍼바이저에서 'IRQ 인터럽트'가 발생하면 17번째 줄에 보이는 0x26aa80 주소로 프로그램 카운터가 분기됩니다. 17번째 줄의 코드는 hyp_irq 레이블로 분기하는 명령어입니다.

이어서 20 ~ 22번째 줄을 보겠습니다.

20  0x26ab00:    17fffb76     b    0x2698d8 <hyp_fiq_invalid>
21  0x26ab04:    d503201f     nop
22  0x26ab08:    d503201f     nop

하이퍼바이저가 구동되는 EL2에서 'FIQ 인터럽트'가 유발되면 20번째 줄에 보이는 0x26ab00 주소로 프로그램 카운터가 분기됩니다. 그런데 XEN 하이퍼바이저는 FIQ를 지원하지 않으므로 hyp_fiq_invalid라는 레이블로 분기되며, 이후 크래시를 유발하는 코드가 실행됩니다.

 

---
[정보] FIQ는 어디서 처리할까?
대부분의 운영체제에서 FIQ는 시큐어 월드의 Trusted 커널에서 받아서 처리하도록 시스템을 설정합니다.
---

이어서 SError 익셉션이 유발되면 실행되는 23 ~ 25번째 줄 코드를 분석하겠습니다.

23  0x26ab80:    17fffb8c     b    0x2699b0 <hyp_error>
24  0x26ab84:    d503201f     nop
25  0x26ab88:    d503201f     nop

EL2에서 SError 익셉션이 유발되면 23번째 줄에 보이는 주소로 프로그램 카운터가 바뀝니다. 'b    0x2699b0 <hyp_error>' 명령어가 실행되면 hyp_error 레이블로 분기되는데, hyp_error 레이블에서는 크래시를 유발하는 루틴이 실행됩니다. 

게스트 OS가 실행되는 EL1에서 발생한 익셉션에 대한 처리 루틴

이번에는 XEN 하이퍼바이저의 익셉션 핸들러 코드 중에서 가장 중요한 루틴인 26 ~ 28번째 줄을 보겠습니다.

26  0x26ac00:    17fffbc5     b    0x269b14 <guest_sync>
27  0x26ac04:    d503201f     nop
28  0x26ac08:    d503201f     nop

게스트 OS가 실행되는 EL1에서 HVC, WFI, WFE 명령어를 실행했을 때 26번째 줄에 있는 0x26ac00 주소로 프로그램 카운터가 분기됩니다. 이런 동작을 "게스트 Exit"라고 합니다. 소프트웨어적으로 EL1에서 EL2로 진입하는 시작점이 26번째 줄이라고 볼 수 있으며, 하이퍼바이저를 분석할 때 가장 많이 리뷰하는 루틴이 게스트 Exit로 실행되는 익셉션 핸들러입니다.

이번에는 29 ~ 31번째 줄을 봅시다. 

29  0x26ac80:    17fffbfe     b    0x269c78 <guest_irq>
30  0x26ac84:    d503201f     nop
31  0x26ac88:    d503201f     nop

게스트 OS에서 ‘IRQ Interrupt’ 익셉션이 유발될 때 실행되는 코드입니다. 29번째 줄과 같이 guest_irq라는 레이블로 분기합니다.
 
EL1에서 실행되는 게스트 OS에서 설정된 인터럽트가 발생하면 일반적으로 EL1(VBAR_EL1)에서 받아 처리합니다. 만약 HCR_EL2 레지스터의 VI와 IMO 비트가 1로 설정된 경우에만 EL2(VBAR_EL2)가 EL1에서 설정된 인터럽트를 받아 처리합니다. 

이어서 EL1에서 FIQ 인터럽트 익셉션이 유발되면 실행되는 32 ~ 34번째 줄을 보겠습니다.

32  0x26ad00:    17fffc13     b    0x269d4c <guest_fiq_invalid>
33  0x26ad04:    d503201f     nop
34  0x26ad08:    d503201f     nop

32번째 줄과 같이 guest_fiq_invalid 레이블로 분기됩니다. XEN 하이퍼바이저는 FIQ를 지원하지 않으므로 guest_fiq_invalid 레이블에서는 크래시를 유발하는 루틴이 실행됩니다.
 
게스트 Exit을 처리하는 루틴 분석 

이어서 게스트 Exit가 실행되면 호출되는 guest_sync 레이블의 코드를 분석하겠습니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/arch/arm/arm64/entry.S
01 guest_sync:
02        /*
03         * Save x0, x1 in advance
04         */
05        stp     x0, x1, [sp, #-(UREGS_kernel_sizeof - UREGS_X0)]
06
07        /*
08         * x1 is used because x0 may contain the function identifier.
09         * This avoids to restore x0 from the stack.
10         */
11        mrs     x1, esr_el2
12        lsr     x1, x1, #HSR_EC_SHIFT           /* x1 = ESR_EL2.EC */
13        cmp     x1, #HSR_EC_HVC64
14        b.ne    guest_sync_slowpath             /* Not a HVC skip fastpath. */
15
16        mrs     x1, esr_el2
17        and     x1, x1, #0xffff                 /* Check the immediate [0:16] */
18        cbnz    x1, guest_sync_slowpath         /* should be 0 for HVC #0 */

먼저 11 ~ 12번째 줄을 보겠습니다.

11        mrs     x1, esr_el2
12        lsr     x1, x1, #HSR_EC_SHIFT           /* x1 = ESR_EL2.EC */

11번째 줄은 익셉션 클래스의 정보를 담고 있는 esr_el2(익셉션 신드롬 레지스터)의 값을 x1 레지스터에 로딩하는 동작입니다. 11번째 줄에서 esr_el2 레지스터의 값을 x1 레지스터에 로딩하는 이유는 무엇일까요? esr_el2 레지스터의 [31:26] 비트에 익셉션이 유발된 세부 원인을 나타내는 익셉션 클래스 비트 정보가 저장돼 있기 때문입니다.

12번째 줄은 x1 레지스터의 값을 #HSR_EC_SHIFT만큼 오른쪽으로 비트 시프트 연산한 결과를 x1 레지스터에 저장합니다. 여기서 HSR_EC_SHIFT는 다음 매크로 선언부와 같이 26입니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/include/asm-arm/processor.h
#define HSR_EC_SHIFT                26

11번째 줄에서 esr_el2 레지스터의 값을 x1 레지스터에 로딩한 다음에 오른쪽으로 26만큼 비트 시프트(12번째 줄) 연산을 수행합니다. 그 결과, 익셉션 클래스를 나타내는 정숫값을 x1 레지스터가 저장합니다.

11 ~ 12번째 줄의 동작은 12번째 줄에 보이는 주석처럼 다음과 같이 표기할 수 있습니다.

 'x1 = ESR_EL2.EC'

ESR_EL2는 EL2에서 액세스하는 익셉션 신드롬 레지스터이고, EC는 익셉션 클래스를 나타냅니다.
 
이어서 13 ~ 14번째 줄을 보겠습니다.
 
13        cmp     x1, #HSR_EC_HVC64
14        b.ne    guest_sync_slowpath             /* Not a HVC skip fastpath. */
 
13번째 줄은 x1 레지스터가 담고 있는 익셉션 클래스가 #HSR_EC_HVC64인지 비교하는 명령어입니다. 13번째 줄 명령어의 실행 결과는 14번째 줄에 영향을 주는데, x1 레지스터의 값이 #HSR_EC_HVC64가 아니면 guest_sync_slowpath 함수로 분기합니다.

 

---
[정보] HSR_EC_HVC64 매크로의 정체는?

여기서 HSR_EC_HVC64 매크로의 정체는 무엇일까요? 다음 코드와 같이 0x16입니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/include/asm-arm/processor.h
#define HSR_EC_HVC64                0x16

그렇다면 HSR_EC_HVC64를 0x16으로 설정한 근거는 무엇일까요? 의문을 풀기 위해서는 Armv8 아키텍처의 스펙 문서를 볼 필요가 있습니다. 다음은 익셉션 클래스와 관련된 Arm 스펙 문서의 내용입니다.

출처: Arm Architecture Reference Manual Armv8, for A-profile architecture https://developer.arm.com/documentation/ddi0487/gb 
D1.10.4 Exception classes and the ESR_ELx syndrome registers 
010110 HVC instruction execution in AArch64 state, when HVC is not disabled

위에 명시된 0b010110(이진수)은 익셉션 클래스를 나타내는 비트 정보로써 16진수로는 0x16입니다. 이처럼 Arm 스펙 문서에서 명시된 정보를 기반으로 어셈블리 명령어를 구현합니다.
---

여기까지 EL2에서 처리되는 익셉션 핸들러의 전반적인 구조를 알아봤습니다. EL2에 존재하는 익셉션 핸들러도 Arm 아키텍처에서 정의된 익셉션 벡터 테이블에 따라 구현됐다는 사실을 알 수 있습니다. 

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

<강의 영상>
 

 

 

이전 절에서 익셉션 벡터 테이블의 스펙을 간단히 리뷰했습니다. 이어서 EL2 관점에서 익셉션 벡터 테이블을 자세히 분석하겠습니다.

EL2 관점에서 분석한 익셉션 벡터 테이블

다음은 하이퍼바이저가 실행되는 EL2 기준에서 본 익셉션 벡터 테이블을 나타낸 표입니다.

 

 

 

 

 

 

 

 

 



먼저 'EL2 with SP_EL0' 행의 내용을 분석합시다. 'EL2 with SP_EL0'는 익셉션 레벨에 상관없이 스택을 설정하는 조건에서 실행되는 익셉션 종류별 오프셋을 나타냅니다. 그런데 대부분의 하이퍼바이저는 이와 같은 익셉션은 지원하지 않습니다.    

EL2에서 발생한 익셉션 분석

이어서 'EL2에서 발생'로 표시된 부분을 봅시다. 0x200, 0x280, 0x300, 0x380은 하이퍼바이저가 구동되는 EL2에서 익셉션이 발생했을 때 익셉션의 종류별로 분기되는 오프셋 주소를 나타냅니다. 

이 내용에 대해 조금 더 자세히 알아볼까요? 만약 EL2에서 실행되는 하이퍼바이저에서 Synchronous 익셉션이 유발되면 다음과 같이 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x200 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다. 

 Synchronous 익셉션 벡터 주소: VBAR_EL2 + 0x200

만약 EL2에서 'IRQ 인터럽트' 익셉션이 유발되면 익셉션 벡터 베이스 주소인 VBAR_EL2에 +0x280 오프셋을 더한 주소로 프로그램 카운터가 다음과 같이 분기됩니다. 

 IRQ 인터럽트 익셉션 벡터 주소: VBAR_EL2 + 0x280

또한 EL2에서 FIQ와 SError 익셉션이 유발되면 같은 방식으로 다음과 같은 주소로 프로그램 카운터가 분기됩니다.

 FIQ 익셉션 벡터 주소: VBAR_EL2 + 0x300
 SError 익셉션 벡터 주소: VBAR_EL2 + 0x380

 

---
[정보] 하이퍼바이저와 FIQ의 관계

대부분의 하이퍼바이저에서는 FIQ 익셉션을 지원하지 않습니다. FIQ는 시큐어 월드에서 실행되는 트러스트 커널에서 받아 처리하도록 시스템을 구성합니다.
---

게스트 OS가 실행되는 EL1에서 발생한 익셉션 분석

이어서 'EL1에서 발생(Aarch64)'로 표시된 부분은 게스트 OS가 구동되는 EL1에서 익셉션이 유발되면 분기되는 오프셋 주소를 나타냅니다. 

EL1에서 실행되는 게스트 OS에서 HVC, WFE, WFI 명령어를 실행하면 Synchronous 익셉션이 유발됩니다. 이때 다음과 같이 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x400 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다. 

 Synchronous 익셉션 벡터 주소: VBAR_EL2 + 0x400

이 같은 동작을 “게스트 Exit 혹은 하이퍼바이저로 트랩된다”라고 설명할 수 있습니다. 

또한 게스트 OS가 실행되는 EL1에서 'IRQ 인터럽트' 익셉션이 유발되면 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x480 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다.

 IRQ 인터럽트 익셉션 벡터 주소: VBAR_EL2 + 0x480

일반적으로 EL1에서 설정한 IRQ 인터럽트는 EL1에 존재하는 익셉션 핸들러에서 처리합니다. 하지만 HCR_EL2.IMO를 1로 설정하면 해당 인터럽트를 EL1에 있는 익셉션 핸들러가 아닌 EL2에 존재하는 익셉션 핸들러에서 받아 처리할 수 있습니다. 물리적인 인터럽트를 EL2가 먼저 받아 이를 EL1에서 실행되는 게스트 OS에 알려야 한다면 가상 인터럽트를 생성해 게스트 OS에 통지할 수 있습니다.  

다음으로 게스트 OS가 실행되는 EL1에서 'FIQ 인터럽트' 익셉션이 유발되면 분기되는 주소를 알아봅시다. 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x500 오프셋을 더한 주소로 다음과 같이 프로그램 카운터가 분기됩니다. 

 FIQ 인터럽트 익셉션 벡터 주소: VBAR_EL2 + 0x500

이전에 설명했지만 대부분의 하이퍼바이저는 FIQ를 지원하지 않습니다. +0x500 오프셋을 적용한 주소에는 크래시를 유발하는 *_invalid와 같은 레이블이 실행되며, 서브루틴에서 크래시를 유발하는 코드가 실행됩니다.

마지막으로 게스트 OS가 실행되는 EL1에서 'SError 인터럽트' 익셉션이 유발되면 익셉션 벡터 베이스 주소인 VBAR_EL2를 기준으로 +0x580 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다. 규칙은 다음과 같습니다.

 SError 익셉션 벡터 주소: VBAR_EL2 + 0x580

일반적으로 EL1에서 설정한 'SError 인터럽트'는 EL1에서 처리되나 HCR_EL2 레지스터를 설정하면 해당 'SError 인터럽트' 익셉션을 EL2에서 받아 처리할 수 있습니다.

'EL1에서 발생(Aarch32)' 행은 EL1에서 실행되는 게스트 OS가 32비트 환경에서 실행될 때 처리되는 익셉션입니다. 최근 대부분의 게스트 OS(예: 리눅스 커널)는 64비트 모드로 실행되므로 'EL1에서 발생(Aarch32)' 행에서 정의된 익셉션은 "하위 호환성을 위해 정의됐다"라는 정도로 알아둡시다.

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

 

 

 

 

 

 

 

 

 

이번에는 HCR_EL2 레지스터를 읽는 예시 코드를 소개합니다. 다음은 XEN 하이퍼바이저에서 호출되는 _show_registers 함수의 어셈블리 명령어입니다.

01 0x25c550 <_show_registers.isra.14>:
02  0x25c550:    stp    x19, x20, [sp, #-96]!
03  0x25c554:    and    w20, w2, #0xff
...
04  0x25c62c:    mrs    x1, hcr_el2
05  0x25c630:    adrp    x0, 284000 <symbols_token_index+0x9700>
06  0x25c634:    add    x0, x0, #0x908
07  0x25c638:     bl    0x23bee0 <printk>

먼저 04번째 줄에 있는 'mrs x1, hcr_el2' 명령어를 분석하겠습니다. 이것은 HCR_EL2 레지스터의 값을 x1 레지스터에 로딩하는 동작입니다. 07번째 줄은 printk 함수를 호출해 HCR_EL2 레지스터의 값을 저장한 x1 레지스터를 출력합니다.
 
이번에는 HCR_EL2 레지스터를 설정하는 예제 코드를 소개합니다. 다음은 XEN 하이퍼바이저가 부팅할 때 호출되는 init_traps 함수의 어셈블리 명령어입니다.

01 0x25c8a0 <init_traps>:
02  0x25c8a0:    adrp    x0, 0x26a000 <guest_irq_compat+0x70>
03  0x25c8a4:    add    x0, x0, #0x800
04  0x25c8a8:    msr    vbar_el2, x0
...
05  0x25c8c8:    mov    x0, #0x38 // #56
06  0x25c8cc:    msr    hcr_el2, x0
07  0x25c8d0:    isb
08  0x25c8d4:    ret
    
먼저 05번째 줄을 보겠습니다. 0x38이란 값을 x0 레지스터에 이동하는 동작입니다. 06번째 줄은 x0 레지스터의 값을 hcr_el2 레지스터에 설정하는 명령어입니다.

 

---
[중요] HCR_EL2 레지스터에 설정하는 0x38의 정체는?

위 코드에서 0x38은 무엇을 의미할까요? 0x38을 2진수인 0b111000으로 표기할 수 있는데, 이는 HCR_EL2 레지스터를 구성하는 비트 필드에 대응됩니다.

HCR_EL2의 3번째 비트는 FMO, 4번째 비트는 IMO, 그리고 5번째 비트는 AMO인데, 이를 이진수로 0b111000으로 표기할 수 있습니다. 해당 비트 필드를 1로 설정하면 비트 필드의 기능을 활성화하게 됩니다. 이 방식으로 HCR_EL2 레지스터를 설정합니다.

참고로 HCR_EL2.IMO와 HCR_EL2.FMO는 IRQ와 FIQ를 가상 인터럽트로 설정하는 비트 필드입니다. HCR_EL2.IMO와 HCR_EL2.FMO 비트 필드인데 1로 설정되면 EL2나 EL1에서 IRQ나 FIQ가 유발되면 EL2에 존재하는 익셉션 핸들러(VBAR_EL2)가 받아 처리합니다. 또한 HCR_EL2.AMO는 EL1 혹은 EL2에서 SError가 유발되면 EL2에서 받아 처리할 수 있는 기능을 제공합니다.

정리하면 HCR_EL2 레지스터에 존재하는 IMO, FMO, AMO 비트 필드는 EL1에서 인터럽트나 SError가 유발되면 EL2에 라우팅되도록 설정하는 기능을 제공합니다.

이번에는 앞에서 소개한 어셈블리 명령어에 대응하는 C 코드를 보겠습니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/arch/arm/traps.c
01 void init_traps(void)
02 {
...
03    /*
04     * Configure HCR_EL2 with the bare minimum to run Xen until a guest
05     * is scheduled. {A,I,F}MO bits are set to allow EL2 receiving
06     * interrupts.
07     */
08    WRITE_SYSREG(HCR_AMO | HCR_FMO | HCR_IMO, HCR_EL2);
09    isb();
10 }

08번째 줄을 봅시다. WRITE_SYSREG 매크로 함수를 사용해 HCR_AMO, HCR_FMO, HCR_IMO를 비트 OR 연산한 결과를 HCR_EL2 레지스터에 쓰는 동작입니다.

HCR_EL2 시스템 레지스터 이외의 다른 시스템 레지스터도 이와 유사한 방식으로 설정합니다.

---

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

 

 

 

 

HCR_EL2 레지스터를 구성하는 비트 필드 정보를 파악하면 "이 비트를 어떻게 설정할 수 있을까?"라는 의문이 생깁니다. Armv8 아키텍처에서는 HCR_EL2 레지스터의 값을 읽고 쓸 수 있는 MRS, MSR 명령어를 제공합니다.

HCR_EL2 레지스터는 읽는 MRS 명령어

먼저 HCR_EL2 레지스터의 값을 읽기 위해서는 다음과 같은 형식의 MRS 명령어를 실행하면 됩니다.

 

 

 

 

 

 


그림 15.7 HCR_EL2 레지스터를 읽는 MRS 명령어

그림 15.7은 HCR_EL2 레지스터의 값을 Xt 레지스터로 로딩하는 명령어입니다. 위 그림에서 보이는 <Xt>는 Armv8 아키텍처에서 정의된 범용 레지스터를 뜻하며, X0 ~ X30 레지스터 중 하나가 지정됩니다. MRS 명령어를 사용하면 HCR_EL2 레지스터의 어떤 비트가 활성화돼 있는지 알 수 있습니다. 

 

---
[중요] MRS 명령어를 해석하는 방법

MRS 명령어를 만나면 그림 15.7의 왼쪽 윗부분에 있는 화살표와 같이 명령어의 가장 오른쪽에서 왼쪽 방향으로 분석할 필요가 있습니다. MRS 명령어의 ‘S’는 시스템 컨트롤 레지스터, ‘R’은 범용 레지스터라고 이해하면 됩니다. 가장 오른쪽에 있는 ‘S’에서 바로 왼쪽에 ‘R’이 있으니 "시스템 컨트롤 레지스터의 값을 범용 레지스터에 로딩한다"라고 해석할 수 있습니다.
---

HCR_EL2 레지스터에 값을 쓰는 MRS 명령어

이어서 HCR_EL2 레지스터에 지정된 값을 쓰는 MSR 명령어를 배워 봅시다.

 

 

 

 

 

그림 15.8 HCR_EL2 레지스터에 값을 써주는 MSR 명령어

그림 15.8과 같이 MSR은 <Xt> 레지스터의 값을 HCR_EL2 레지스터에 쓰는 명령어입니다. 위 그림에서 보이는 <Xt>는 Armv8 아키텍처에서 정의된 범용 레지스터를 뜻하며, X0 ~ X30 레지스터가 지정될 수 있습니다. 이 명령어로 HCR_EL2 레지스터를 구성하는 세부 비트를 설정할 수 있습니다.

 

---
[중요] MSR 명령어를 해석하는 방법

MSR 명령어는 그림 15.8의 왼쪽 윗부분에 있는 화살표와 같이 명령어의 가장 오른쪽에서 왼쪽 방향으로 분석할 필요가 있습니다. MSR 명령어의 ‘R’을 범용 레지스터, 바로 왼쪽에 있는 ‘S’를 시스템 컨트롤 레지스터라고 이해하면 됩니다. 가장 오른쪽에 ‘R’이 있고, 왼쪽에 ‘S’가 있으니 "범용 레지스터의 값을 시스템 컨트롤 레지스터에 써준다"라고 해석할 수 있습니다.

---

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

<강의 영상>
 

HCR_EL2는 ‘Hypervisor Configuration Register, EL2’의 약자로서 하이퍼바이저의 세부 동작을 설정하는 가장 중요한 레지스터 중 하나입니다. HCR_EL2는 EL1에서 EL2로 진입(트랩)하는 방식을 설정하는 비트 필드로 구성돼 있습니다. HCR_EL2 레지스터의 접미사에 있는 EL2는 이 레지스터에 접근할 수 있는 최소 익셉션 레벨이므로 EL2 혹은 EL3에서만 접근할 수 있습니다.  

다음 그림은 HCR_EL2 레지스터의 비트 필드 정보입니다.

 

 

 

 

 

 

 

 

 

 

 

그림 15.6 HCR_EL2 레지스터의 비트 필드 

다른 레지스터와 같이 레지스터를 구성하는 비트를 1로 설정하면 해당 비트 필드와 관련된 동작이 수행됩니다. HCR_EL2 레지스터를 구성하는 비트 필드 중에 중요한 내용을 소개하겠습니다.
 
FMO, bit [3]

물리적인 FIQ 라우팅 방식을 설정하는 비트 필드입니다. 각 비트에 따라 다음과 같이 설정됩니다.

 0b0: 가상 FIQ 인터럽트를 비활성화하므로 물리적인 FIQ가 EL2로 라우팅되지 않음
 0b1: 물리적인 FIQ가 유발되면 EL2로 라우팅됨

일반적으로 FIQ는 시큐어 인터럽트로 처리되므로 EL3 모니터 혹은 시큐어 EL1에서 받아 처리합니다.
 
IMO, bit [4]

IMO는 물리적인 IRQ 라우팅 방식을 설정하는 비트 필드입니다. 각 비트에 따라 다음과 같이 설정됩니다.

 0b0: 가상 IRQ 인터럽트를 비활성화하므로 물리적인 IRQ가 EL2로 라우팅되지 않음
 0b1: 물리적인 IRQ가 유발되면 EL2로 라우팅됨. EL2나 EL1에서 인터럽트가 유발되면 EL2에 존재하는 익셉션 핸들러(VBAR_EL2)가 받아 처리

만약 SCR_EL3.IRQ 비트가 1로 설정되면 EL3에 존재하는 익셉션 핸들러(VAR_EL3)가 받아 인터럽트를 처리합니다. 만약 IMO 비트가 0으로 설정되면 EL2보다 낮은 익셉션 레벨에서 설정된 인터럽트를 EL2가 받아 처리하지 않습니다.

AMO, bit [5]

이 비트는 EL1에서 발생한 Asynchronous 익셉션을 EL2에 위치한 익셉션 핸들러가 받아 처리하기 위한 용도로 사용됩니다. AMO 비트가 1로 설정됐으면 EL2나 EL2보다 낮은 익셉션 레벨에서 Asynchronous 혹은 SError 인터럽트가 발생하면 EL2로 트랩됩니다. 

TWI, bit [13]

WFI 명령어에 대한 트랩을 설정하는 비트 필드로, 각 비트에 따라 다음과 같이 설정됩니다.

 0b0: WFI 명령어를 실행하면 EL2로 트랩되지 않고 Arm 코어가 저전력 모드로 진입
 0b1: EL0 혹은 EL1에서 WFI 명령어를 실행하면 트랩이 발생하면서 EL2로 익셉션 레벨이 변경 

TWE, bit [14]

TWI 비트와 비슷한 용도로 WFE 명령어에 대한 트랩을 설정하는 비트 필드입니다. 각 비트에 따라 다음과 같이 설정됩니다.

 0b0: WFE 명령어를 실행하면 EL2로 트랩되지 않고 Arm 코어가 저전력 모드로 진입
 0b1: EL0 혹은 EL1에서 WFE 명령어를 실행하면 트랩이 발생하면서 EL2로 익셉션 레벨이 변경 

TSC, bit [19]

SMC 명령어와 관련된 트랩 방식을 설정하는 비트 필드입니다.

 0b0: EL1에서 SMC 명령어를 실행하면 EL3에 존재하는 모니터 모드로 진입
 0b1: EL1에서 SMC 명령어를 실행하면 EL2로 트랩

여러 게스트 OS에서 SMC 명령어를 실행하면 동시다발적으로 EL3에 진입할 수 있어 누군가 이를 받아 처리해야 합니다. 이때 TSC 비트 필드를 1로 설정하면 여러 게스트 OS에서 SMC 명령어를 통해 EL2에서 실행되는 하이퍼바이저에 존재하는 익셉션 핸들러로 트랩됩니다. 이를 SMC 명령어 후킹이라고 합니다.

TVM, bit [26]

가상 메모리를 제어하기 위한 트랩을 설정하는 비트입니다. TVM 비트가 1로 설정될 경우 EL1에서 다음과 같은 메모리를 제어하는 레지스터에 어떤 값을 쓰면 EL2로 트랩됩니다. 

 SCTLR_EL1
 TTBR0_EL1
 TTBR1_EL1
 TCR_EL1
 ESR_EL1
 FAR_EL1
 AFSR0_EL1
 AFSR1_EL1
 MAIR_EL1
 AMAIR_EL1
 CONTEXTIDR_EL1

EL2로 트랩될 때 Arm 코어는 하드웨어적으로 ESR_EL2(익셉션 신드롬 레지스터)의 [31:26] 비트에 0x18 값을 씁니다.

TGE, bit [27]

TGE 비트 필드는 EL1에서 발생한 익셉션을 EL2로 라우팅할 때 설정합니다. 비트에 따라 다음과 같이 설정됩니다.

 0b0: EL1에서 발생한 익셉션이 EL2로 라우팅되지 않음. EL1에서 익셉션이 발생하면 EL1에 존재하는 익셉션 벡터 주소(VBAR_EL1 기준)로 프로그램 카운터가 분기
 0b1: EL1에서 발생한 익셉션이 EL2로 라우팅됨  

운영체제의 커널에서 실행되는 시스템 정보를 EL2에서 실행되는 하이퍼바이저가 후킹할 때도 사용됩니다. 

HCD, bit [29]

HCD 비트는 HVC 명령어인 하이퍼바이저 콜에 대한 마스킹(비활성화)을 설정하는 기능입니다. HCD 비트가 0으로 설정되면 HVC 명령어가 활성화됩니다. HCD 비트가 1으로 설정되면 HVC 명령어는 비활성화됩니다. 이 조건에서 HVC 명령어를 실행하면 정의되지 않은 명령어(Undefined Instruction)로 간주되어 익셉션이 유발됩니다.

이번 절에서는 HCR_EL2 시스템 레지스터를 이루는 비트 필드에 대해 알아봤습니다. HCR_EL2 시스템 레지스터를 통해 하이퍼바이저의 세부 동작을 설정할 수 있어 하이퍼바이저를 초기화할 때 HCR_EL2는 반드시 체크해야 합니다. 

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

 

 

 

 

많은 개발자들은 “HVC 명령어를 통해서만 EL1에서 EL2로 진입할 수 있다”라고 알고 있습니다. 사실 그렇지는 않습니다. WFE와 WFI 명령어를 실행해도 EL1에서 실행되는 게스트 OS에서 EL2로 진입할 수 있습니다. 

---
[정보] WFI와 WFE 명령어

WFI와 WFE 명령어는 Arm 아키텍처에서 지원하는 저전력 기능 중 하나입니다. 리눅스 커널이나 RTOS 관점에서 루틴을 분석하면 "함수가 동시다발적으로 아주 빠르게 실행된다"라고 느끼지만 이를 실행하는 Arm 코어 입장에서는 여러 함수에서 특정 루프(예: while)를 반복하면서 데이터가 특정 조건을 만족할 때까지 기다리는 동작을 자주 수행합니다.

이 상황에서 Arm 코어가 저전력 모드(스탠바이)에 진입하면 전력을 최적화해 Arm 코어를 실행할 수 있습니다. 시스템에 하나의 운영체제만 존재하는 환경에서는 WFE, WFI 명령어가 저전력 모드용으로 사용됩니다. 

가상화 관점에서는 게스트 OS에서 WFI 혹은 WFE 명령어가 실행되면 익셉션이 유발되어 EL2(하이퍼바이저 구동)로 진입한다고 볼 수 있습니다. 하이퍼바이저와 같이 시스템에 2개 이상의 운영체제가 실행되는 환경에서 HCR_EL2 레지스터의 TWE 혹은 TWI 비트를 1로 설정한 다음에 WFE, WFI 명령어를 실행하면 HVC 명령어처럼 EL2로 진입합니다.  
---

Arm 스펙 문서에서 WFE, WFI 명령어 알아보기

이어서 Arm 아키텍처 문서를 보면서 WFE, WFI 명령어에 대해 더 자세히 알아봅시다.

EL2 provides the following traps for WFE and WFI instructions:

• HCR_EL2.TWE:
1: Any attempt to execute a WFE instruction at Non-secure EL1 or EL0 is trapped to 
EL2, if the instruction would otherwise have caused the PE to enter a low-power 
state.
  0: Non-secure EL1 or EL0 execution of WFE instructions is not trapped to EL2.

• HCR_EL2.TWI
1: Any attempt to execute a WFI instruction at Non-secure EL1 or EL0 is trapped to 
EL2, if the instruction would otherwise have caused the PE to enter a low-power 
state.
  0: Non-secure EL1 or EL0 execution of WFI instructions is not trapped to EL2.

스펙 문서의 내용을 다음과 같이 해석할 수 있습니다.

 EL2는 WFE와 WFI 명령어에 대한 트랩을 지원한다. 
 HCR_EL2 레지스터의 TWE와 TWI 비트가 1로 설정된 경우 WFE 혹은 WFI 명령어를 실행하면 EL2로 트랩된다.
 만약 HCR_EL2 레지스터의 TWE와 TWI 비트가 0으로 설정됐으면 기존의 저전력 모드로 진입하는 동작을 수행한다.

이어서 HVC, WFE, WFI 명령어를 실행하면 어떤 방식으로 EL2에 진입하는지 알아봅시다. 

HVC, WFE, WFI 명령어와 Synchronization 익셉션

지금까지 설명한 내용을 정리하면 HCR_EL2.TWE와 HCR_EL2.TWE가 1인 조건에서 HVC, WFE, WFI와 같은 명령어를 실행하면 EL1에서 EL2로 진입됩니다. 이 부분까지 읽으면 다음과 같은 의문이 생길 수 있습니다.

    "HVC, WFE, WFI와 같은 명령어를 실행하면 어떻게 EL2에 진입할까?"

HVC, WFE, WFI와 같은 명령어를 실행하면 Arm 코어는 Synchronous 익셉션을 유발하며, 다음과 같은 규칙에 따라 프로그램 카운터를 업데이트합니다.

 프로그램 카운터: VBAR_EL2 + 0x400

여기서 VBAR_EL2는 EL2에 정의된 익셉션 벡터 테이블의 베이스 주소이고 0x400은 Synchronous 익셉션의 오프셋 정보입니다.

---
[정보] EL2 관점에서 해석한 익셉션 벡터 테이블

하이퍼바이저가 실행되는 EL2 관점에서 해석한 익셉션 벡터 테이블의 세부 내용은 15.4절 ‘가상화와 익셉션 벡터 테이블’을 참고하세요.
---

HVC, WFE, WFI 명령어와 익셉션 클래스

HVC, WFE, WFI 같은 명령어를 실행하면 EL2에 존재하는 같은 익셉션 핸들러에서 처리된다고 볼 수 있습니다. 그렇다면 EL1에서 HVC 명령어가 실행되어 EL2에 진입했는지 혹은 WFE/WFI와 같은 명령어를 실행해 EL2에 진입했는지 어떻게 알 수 있을까요? 

Arm 코어는 HVC, WFE 와 WFI 같은 명령어를 실행하면 Synchronous 익셉션을 유발하면서 다음 표와 같은 익셉션 클래스를 ESR_EL2의 [36:21] 비트에 업데이트합니다.


표 15.1 HVC, WFE, WFI 명령어와 익셉션 클래스

HVC, WFE, WFI 명령어를 실행하면 분기되는 같은 익셉션 핸들러에서 표 15.1에서 소개한 익셉션 클래스 비트 정보는 ESR_EL2 레지스터의 [31:26] 비트에서 읽어 HVC, WFE, WFI 명령어에 따라 다른 방식으로 처리할 수 있습니다.

이어지는 절에서 Arm 아키텍처 관점에서 하이퍼바이저의 세부 동작을 설정할 수 있는 HCR_EL2 레지스터에 대해 알아봅시다. 

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

<강의 영상>
 

HVC는 하이퍼바이저 콜(Hypervisor Call)이라고 하며, EL1에서 HVC 명령어를 실행하면 EL2로 진입합니다. 

Arm 스펙 문서에서 HVC 명령어 알아보기

먼저 Arm 스펙 문서에서 HVC 명령어를 설명한 부분을 소개합니다. 

출처: Arm Architecture Reference Manual Armv8, for A-profile architecture https://developer.arm.com/documentation/ddi0487/gb 
C6.2.94 HVC
Hypervisor Call causes an exception to EL2. Software executing at EL1 can use this instruction to call the hypervisor to request a service.

스펙 문서의 내용은 다음과 같이 해석할 수 있습니다.

 하이퍼바이저 콜은 EL2로 익셉션을 유발한다.
 EL1에서 실행되는 소프트웨어는 HVC 명령어를 사용해 하이퍼바이저에게 어떤 서비스를 요청한다.

EL1에서 HVC 명령어를 실행해 하이퍼바이저가 구동되는 EL2로 진입하는 동작을 하이퍼바이저 콜이라고 합니다. 여기서 HVC 명령어를 실행해 하이퍼바이저 콜을 유발하는 소프트웨어는 EL1에서 실행되는 리눅스 커널이나 RTOS 커널입니다.

EL2에서 실행되는 하이퍼바이저 위에서 구동되는 여러 게스트 OS(EL1)가 있는데, 하이퍼바이저에게 어떤 서비스를 요청하기 위해 HVC 명령어를 실행합니다. 이런 동작을 게스트 Exit라고 합니다.

---
[정보] EL1에서 EL2로 진입하는 이유는 무엇일까?

EL1에서 실행되는 다수의 게스트 OS의 리소스를 제어하고 관리하는 권한이 EL2에 있는데, EL2에서 실행되는 하이퍼바이저에게 어떤 서비스를 요청하기 위해 EL1에서 EL2로 진입합니다. 이를 위해서는 EL1에서 HVC, WFE, WFI 명령어를 실행해야 하는데, 이 과정에서 트랩이 유발됩니다. 여기서 트랩은 익셉션 관점에서 소프트웨어로 유발되는 Synchronous 익셉션으로 볼 수 있습니다. 
---

HVC 명령어의 형식

이어서 HVC 명령어의 형식을 알아봅시다.

 

 

그림 15.5 HVC 명령어의 형식

대부분 어셈블리 명령어가 실행될 때 범용 레지스터를 사용해 명령어의 입력 값이 전달되거나 실행 결과가 업데이트됩니다. 그런데 HVC 명령어의 형식을 보면 범용 레지스터는 보이지 않습니다. HVC 명령어 다음에 #imm과 같이 상수형 인자를 지정할 수 있는데 #imm의 범위는 0 ~ 65535입니다. 

HVC 명령어를 실행하기 전에 X0 ~ X7 레지스터에 아규먼트를 저장해 전달하는 방식을 HVC calling convention이라고 합니다.

HVC 명령어가 실행되기 위한 조건

그런데 HVC 명령어를 실행하면 항상 EL2로 진입할까요? 항상 그렇지는 않습니다. HVC 명령어가 실행되려면 HCR_EL2를 구성하는 비트 필드가 설정돼야 하는데, 이를 Arm 스펙 문서에서 확인할 수 있습니다. 

The HVC instruction is UNDEFINED:

 When EL3 is implemented and SCR_EL3.HCE is set to 0.
 When EL3 is not implemented and HCR_EL2.HCD is set to 1.
 When EL2 is not implemented.
 At EL1 if EL2 is not enabled in the current Security state.
 At EL0.

 

출처: Arm Architecture Reference Manual Armv8, for A-profile architecture https://developer.arm.com/documentation/ddi0487/gb 

위 내용은 다음과 같이 요약할 수 있습니다.

HVC 명령어는 다음과 같은 조건에서 Undefined(정의되지 않는) 명령어로 간주된다.

 EL3가 구현됐는데 SCR_EL3 레지스터의 HCE 비트가 0으로 설정된 경우
 EL3가 구현돼 있지 않고 HCR_EL2 레지스터의 HCD 비트가 1로 설정된 경우
 EL2(하이퍼바이저)가 구현되지 않은 경우
 현재 Security 상태에서 EL2가 활성화되지 않은 경우에 실행되는 EL1
 EL0 익셉션 레벨

따라서 HVC 명령어를 실행했는데 EL2로 진입하지 못하면 위에 명시된 조건을 확인합시다.

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>

 

 

+ Recent posts