Linux v6.18.37 · AArch64 KVM · 일반 VM과 사용자 공간 장치 모델

게스트의 시스템 호출은 어디로 갑니까?

ARM64의 SVC, Stage-2 fault, KVM_RUN 반환을 레지스터와 주소의 변화로 구분합니다.

게스트 커널 진입과 호스트 복귀를 나누어 봅니다

가상 머신에서 프로그램이 getpid를 호출하면 보통 그 가상 머신의 Linux 커널이 처리합니다. 시스템 호출마다 호스트 커널이나 QEMU가 대신 getpid를 실행하는 구조가 아닙니다. 게스트 안에서 사용자 모드와 커널 모드가 바뀌는 사건과, 게스트 실행을 멈추고 호스트가 개입하는 사건을 구분해야 합니다.

경계어떤 사건입니까?이번 예의 처리 주체
게스트 EL0 → 게스트 EL1사용자 프로그램의 SVC로 커널 서비스를 요청합니다.게스트 Linux 커널입니다.
게스트 실행 → EL2 처리Stage-2 fault나 설정된 trap 등의 사건으로 하이퍼바이저 처리가 필요합니다.EL2 진입 코드와 KVM입니다.
KVM_RUN → 호스트 사용자 공간KVM이 사용자 공간의 도움을 받거나 실행을 중단해야 합니다.QEMU 같은 VMM입니다. VMM은 가상 머신을 관리하는 프로그램입니다.

이 글은 중첩 가상화와 보호 VM을 제외하고, ARM64 Linux를 실행하는 일반 VM을 설명합니다. 예제의 SVC에는 추가 trap·debug 설정이 없으며, 신호나 추적 도구가 시스템 호출의 결과를 바꾸지 않는다고 가정합니다. 게스트가 VM을 다시 실행하는 중첩 가상화에서는 SVC를 가로채는 경로도 있습니다. v6.18.37의 KVM handle_svc도 그 경우를 처리합니다. KVM의 SVC·예외 처리

세 줄로 getpid를 요청해 봅니다

아래는 게스트의 64비트 사용자 프로그램 안에 있다고 가정한 설명용 코드입니다. 앞의 주소는 각 명령의 위치를 보이기 위한 값이며 어셈블리 문법의 일부가 아닙니다. 실제 프로그램의 배치 주소와 PID는 실행할 때 달라집니다.

0x00400000:  mov x8, #172
0x00400004:  svc #0
0x00400008:  mov x19, x0
이 줄에서 하는 일이 값은 어디서 옵니까?
mov x8, #172정수 172를 x8에 넣고 x8의 기존 값을 덮어씁니다. 메모리에 172바이트를 복사하는 명령이 아닙니다.이 버전의 AArch64 Linux getpid 시스템 호출 번호입니다. 주소나 PID가 아닙니다.
svc #0동기 예외를 발생시켜 게스트 커널의 시스템 호출 경로로 진입합니다.이 ABI에서 서비스 선택은 x8의 번호로 합니다. #0을 getpid 번호로 해석하지 않습니다.
mov x19, x0커널에서 돌아온 뒤 x0의 정숫값을 x19에 복사합니다. x0는 그대로이고 x19는 덮어씁니다.x0에는 이 호출의 결과인 게스트 프로세스의 PID가 들어옵니다. x0가 가리키는 메모리를 읽는 동작은 없습니다.

getpid는 이 예에서 x0~x5의 인자를 요구하지 않습니다. 따라서 호출 전 x0의 값은 이 서비스의 입력으로 사용하지 않습니다. 다른 시스템 호출은 x0~x5를 인자로 사용할 수 있으므로 같은 결론을 모든 호출에 적용하면 안 됩니다. getpid의 결과는 게스트의 PID namespace에서 보이는 프로세스 ID이며, 호스트 QEMU 프로세스나 vCPU thread의 ID가 아닙니다. getpid 번호 정의 x8을 읽는 do_el0_svc getpid의 반환값

명령을 실행하면서 무엇이 바뀝니까?

게스트 getpid 호출: PID가 42라고 가정한 예

1. 호출 번호를 넣습니다

실행 위치·레지스터mov x8, #172 실행 뒤
현재 실행게스트 EL0입니다. 다음 명령의 PC는 0x00400004입니다.
x8172: getpid를 선택할 번호입니다.
x0호출 전 코드가 남긴 값입니다. 이 getpid의 입력으로 쓰지 않습니다.

PC는 다음에 실행할 명령의 주소입니다. 명령을 실행하기 전 x8의 실제 값은 앞선 코드에 달려 있지만, 이 mov 뒤에는 172로 정해집니다.

2. SVC 예외를 받습니다

위치보관하거나 처리하는 값
실행 위치게스트 EL1의 예외 처리 경로입니다.
ELR_EL1돌아갈 다음 명령 주소 0x00400008을 보관합니다.
SPSR_EL1예외 전의 게스트 EL0 실행 상태를 보관합니다.
커널의 pt_regs어셈블리 진입 코드가 사용자 레지스터와 복귀 상태를 메모리에 저장합니다.

SVC 자체가 PID를 계산해 x0에 넣는 것은 아닙니다. 커널 코드가 x8로 서비스를 고른 뒤 결과를 준비합니다. 커널 내부의 현재 x0와 pt_regs에 저장된 사용자 x0도 구분해야 합니다.

3. 게스트 커널에서 돌아옵니다

실행 위치·레지스터일반적인 정상 복귀 뒤
현재 실행게스트 EL0, PC = 0x00400008입니다.
x0이 예의 PID 42입니다.
QEMU이 시스템 호출을 대신 처리하러 돌아온 상태로 가정하지 않습니다.

게스트 커널이 사용자 레지스터의 반환값을 준비하고 예외 복귀 경로가 이를 복원합니다. 실제 PID가 731이라면 x0에는 731이 들어옵니다. 42는 고정 규칙이 아닙니다.

4. 결과를 x19에 보관합니다

레지스터mov x19, x0 실행 뒤
x1942로 바뀝니다. x19의 호출 전 값은 덮어씁니다.
x042가 유지됩니다.
PC0x0040000c로 진행합니다.

이 마지막 mov는 레지스터 값 복사입니다. 메모리를 가리키는 화살표나 다른 CPU로의 전달로 읽지 않습니다.

단계는 설명용 명령의 실행과 정상 복귀 순서입니다. EL0 → EL1은 같은 게스트 안의 실행 권한 변화입니다. PID 42와 코드 주소는 예제 값이며 실제 장치에서 측정한 결과가 아닙니다.

v6.18.37의 entry-common.c는 SVC64를 el0_svc로 연결합니다. syscall.c는 저장된 x8로 호출을 선택하고, syscall_set_return_value는 저장된 사용자 x0를 갱신합니다. entry.S는 복귀 주소·상태와 레지스터를 복원합니다. 따라서 “예외가 생겼다”는 말만 보고 EL2 또는 QEMU로 갔다고 판단하지 않습니다. 게스트 커널의 SVC 진입 사용자 x0에 반환값 저장 예외 진입·복귀 어셈블리

호스트 커널이 EL2에 있으면 무엇이 다릅니까?

일반적인 구성호스트 Linux 커널게스트 Linux 커널게스트 사용자 프로그램
nVHEEL1에서 실행하고 별도의 EL2 처리 코드를 이용합니다.EL1EL0
VHEEL2에서 실행합니다.EL1EL0

같은 EL1이라는 이름을 써도 nVHE의 호스트 커널과 게스트 커널이 같은 문맥을 공유하는 것은 아닙니다. KVM은 실행 대상을 바꿀 때 레지스터·주소 변환·예외 처리 상태를 전환합니다. 반대로 호스트가 VHE로 EL2에서 실행한다고 해서 게스트의 모든 커널 코드까지 EL2에서 실행되는 것도 아닙니다. nVHE 문맥 전환 VHE 문맥 전환

VHE에서 HCR_EL2의 E2H와 TGE는 호스트용 실행 환경과 EL0 예외 경로를 구분하는 데 관여합니다. Linux의 HCR_HOST_VHE_FLAGS에는 두 비트가 포함됩니다. 이 매크로는 빌드할 때 정해지는 비트 조합이고, 실제 상태 변화는 실행 중 그 값을 HCR_EL2에 기록할 때 일어납니다. 게스트로 들어갈 때와 호스트로 돌아올 때의 설정을 같은 것으로 읽으면 안 됩니다. HCR host·guest 값 trap 활성화·해제

예전 EL2 설명에서 “가상 주소 영역이 하나뿐”이라고 읽었다면 VHE 적용 전후를 확인해야 합니다. VHE는 호스트 커널을 EL2에서 실행하기 위해 두 영역과 ASID 지원을 추가합니다. ASID는 주소 공간을 구별하는 식별자입니다. Arm의 해당 입문 문서 8절도 이 차이를 따로 설명합니다. Arm의 VHE 설명 · 8절

EL2로 나온 뒤에도 QEMU로 돌아오지 않을 수 있습니다

QEMU의 vCPU thread가 KVM_RUN을 호출하면 호스트 커널 안에서 게스트를 실행합니다. 실행 도중 EL2 처리가 필요해져도 KVM이 해결한 뒤 같은 KVM_RUN 안에서 게스트로 다시 들어갈 수 있습니다. 따라서 guest exit 횟수와 사용자 공간으로 돌아온 횟수는 같지 않습니다.

한 번의 KVM_RUN에서 일어날 수 있는 일
  1. 호스트의 vCPU thread가 KVM_RUN을 호출합니다.

    vcpu_fd는 실행할 가상 CPU를 식별하는 파일 descriptor입니다.

    커널에 실행을 요청합니다.

  2. KVM이 게스트 실행 상태를 준비합니다.

    일반 레지스터와 주소 변환, 가상 interrupt 등의 상태를 준비하고 진입합니다.

    게스트가 직접 명령을 실행합니다.

  3. 처리가 필요한 사건으로 게스트에서 나옵니다.

    KVM은 원인을 살펴봅니다. 커널 안에서 해결되면 다음 반복에서 다시 진입할 수 있습니다.

    사용자 공간 처리가 필요한 경우에만 아래로 진행합니다.

  4. KVM_RUN이 VMM으로 반환합니다.

    예를 들어 사용자 공간 장치 모델이 MMIO 응답을 만들어야 합니다.

화살표는 제어가 넘어가는 순서입니다. 커널에서 해결한 경우에는 세 번째 단계에서 두 번째 단계로 돌아갑니다. 모든 guest exit가 마지막 단계까지 간다는 뜻은 아닙니다.

/* v6.18.37 arm.c의 반복 조건과 handler 호출입니다.
 * 사이의 준비·진입·복원 코드는 생략했습니다. */
while (ret > 0) {
    /* ... */
    ret = handle_exit(vcpu, ret);
}
이 내부 handler의 결과호출자가 하는 일
ret > 0실행 loop를 이어갑니다. 다음 반복에서 요청·신호 등을 다시 확인하므로 무조건 즉시 진입한다는 뜻은 아닙니다.
ret == 0사용자 공간에 반환할 사건을 준비한 정상 경로입니다. VMM은 exit_reason을 확인합니다.
ret < 0오류 경로입니다. 사용자 공간 ioctl의 실패와 errno로 이어집니다.

이 ret 규칙은 handle_exit와 실행 loop 사이의 내부 반환 규칙입니다. 사용자 프로그램에서 ioctl(KVM_RUN)이 1을 돌려주면 계속 실행된다는 뜻이 아닙니다. 사용자 공간은 ioctl의 성공·실패를 먼저 확인해야 합니다. 또한 kvm_io_bus_read 같은 helper의 성공값 0도 위 handler의 0과 역할이 다릅니다. 실행 loop handler 반환 규칙

같은 load라도 어느 주소 변환에서 막혔는지 봅니다

일반 RAM 읽기를 예로 들어 보겠습니다. 두 변환 모두 4 KiB 페이지를 사용하고, 권한·메모리 속성·정렬 조건이 맞으며, 필요한 페이지 표도 접근 가능하다고 가정합니다. 게스트 가상 페이지 0x00400000은 IPA 페이지 0x80000000에, 그 IPA는 호스트 물리 페이지 0x120000000에 대응한다고 정했습니다. IPA는 이 VM이 물리 주소로 취급하는 주소입니다.

주소 0x00400128의 RAM을 읽는 예
  1. 게스트 VA: 0x00400128

    게스트 명령이 사용하는 가상 주소입니다. 페이지 안의 offset은 0x128입니다.

    게스트 Stage-1 표의 대응을 적용합니다.

  2. IPA: 0x80000128

    게스트가 관리하는 물리 주소 공간의 주소입니다. 호스트의 실제 물리 주소와 구별합니다.

    KVM이 관리하는 Stage-2 대응을 적용합니다.

  3. 호스트 PA: 0x120000128

    두 변환의 조건을 만족했을 때 읽을 최종 RAM 위치입니다.

화살표는 주소 변환 관계입니다. CPU가 세 주소를 일반 레지스터 세 개에 차례로 넣거나 데이터를 두 번 복사한다는 뜻은 아닙니다. 주소는 설명용 값이며 TLB가 변환을 보관하면 매번 모든 표를 읽지 않을 수 있습니다.

상황누가 먼저 처리합니까?이후 가능한 결과
게스트 Stage-1에 매핑이 없습니다.보통 게스트 커널이 자신의 page fault를 처리합니다.게스트 RAM 할당·매핑 등으로 해결하거나 잘못된 사용자 접근에 signal을 보낼 수 있습니다.
유효한 게스트 RAM이지만 Stage-2 매핑이 아직 없습니다.EL2로 나와 KVM이 memslot과 backing memory를 확인합니다.매핑을 준비한 뒤 fault를 일으킨 명령을 재시도할 수 있습니다.
사용자 공간 모델이 담당하는 가상 장치 주소입니다.KVM이 접근 정보를 준비합니다.커널에서 처리하지 못한 접근은 KVM_EXIT_MMIO로 VMM에 전달할 수 있습니다.
범위·권한·접근 종류를 허용하지 않습니다.해당 fault 경로가 정책과 원인을 판단합니다.예외 주입 또는 오류가 될 수 있습니다. 매핑이 없다는 이유만으로 RAM을 새로 주지는 않습니다.

게스트 페이지 테이블 자체도 메모리에 있으므로, Stage-1 표를 읽는 도중 Stage-2 fault가 생기는 경우도 있습니다. 위 그림은 최종 데이터 주소를 이해하기 위한 단순한 예입니다. “Stage-2 fault이면 Stage-1은 언제나 완전히 끝났다”는 일반 규칙으로 읽지 않습니다. v6.18.37의 kvm_handle_guest_abort도 표 탐색 중 fault인지를 별도로 검사합니다. 게스트 abort와 S1PTW 처리

memslot은 KVM에 등록한 게스트 물리 메모리 범위입니다. 일반 사용자 메모리 기반 VM에서는 그 범위와 호스트 사용자 주소 HVA를 연결합니다. HVA는 KVM이 RAM의 backing page를 찾는 데 쓰며, 게스트의 하드웨어 변환을 VA → IPA → HVA → PA라는 세 단계로 만드는 것은 아닙니다. 실제 AArch64 구현 경로도 v6.18.37에서는 arch/arm64/kvm/mmu.c입니다.

가상 장치에서 읽은 값은 언제 x2에 들어갑니까?

이번에는 게스트 커널이 가상 장치의 32비트 상태 register를 읽는다고 가정합니다. 게스트와 호스트는 little-endian이고, 단일 AArch64 LDR의 정상 MMIO 처리를 예로 듭니다. x1은 장치로 매핑된 게스트 VA이며, 대응하는 IPA는 0x09000000입니다. 이 주소는 사용자 공간의 설명용 장치 모델이 담당합니다. 실제 보드의 register 주소나 장치 기능을 지정한 것이 아닙니다.

ldr w2, [x1]

x1의 숫자를 w2에 복사하는 mov가 아닙니다. x1을 주소로 사용해 4바이트를 읽고 결과를 w2에 넣으려는 명령입니다. 완료되면 x2의 상위 32비트는 0이 됩니다. 이 예에서 접근 전 x2는 0xa5, 장치 모델이 돌려줄 값은 0x12345678이라고 정했습니다.

MMIO 읽기: 요청, 사용자 공간 응답, 게스트 레지스터 반영

1. 게스트가 LDR을 시도합니다

항목아직 접근이 완료되지 않은 상태
x1가상 장치 register를 가리키는 게스트 VA입니다.
저장될 결과아직 없습니다. 게스트의 저장된 x2는 이 예에서 0xa5입니다.
예외 원인Stage-2 경로에서 이 접근을 장치 emulation으로 처리하게 됩니다.

예제에서는 KVM이 syndrome에서 접근 폭 4바이트, 읽기 방향, 대상 register 2번을 해석할 수 있다고 가정합니다. 모든 abort에 이 정보가 있는 것은 아닙니다.

2. KVM이 VMM에 읽기를 요청합니다

kvm_run의 항목이 예의 값
exit_reasonKVM_EXIT_MMIO: 사용자 공간의 장치 처리가 필요합니다.
mmio.phys_addr0x09000000: fault IPA입니다. 호스트 RAM의 PA가 아닙니다.
mmio.len / mmio.is_write4 / 0: 4바이트 읽기입니다.

kvm_run은 커널과 VMM이 공유하는 메모리 구조체입니다. CPU의 하드웨어 register 이름이 아닙니다. 이때 VMM이 게스트 x2를 직접 덮어쓰는 방식으로 설명하지 않습니다.

3. VMM이 응답 바이트를 채웁니다

위치VMM의 처리 뒤
mmio.data의 처음 4바이트78 56 34 12: 예제 값 0x12345678의 little-endian 바이트입니다.
게스트 저장 register x2응답 buffer를 채운 것만으로 갱신이 끝난 것은 아닙니다.

VMM은 같은 vCPU에 KVM_RUN을 다시 호출해 미완료 MMIO 처리를 커널이 끝내도록 합니다. 장치 모델의 요청·응답과 실제 외부 장치의 작업 완료도 별개의 개념입니다.

4. KVM이 게스트 상태에 결과를 반영합니다

항목정상 MMIO 완료 뒤
게스트 x20x0000000012345678입니다. w2의 32비트 읽기 결과와 상위 0이 반영됩니다.
게스트 PCemulation이 끝난 LDR 다음 명령을 가리키도록 조정됩니다.
다음 실행실행 조건을 다시 확인한 뒤 게스트를 진행할 수 있습니다.

일반 RAM의 지연 매핑은 원래 load를 재시도하지만, 이 MMIO 경로는 이미 emulation한 load를 다시 수행하지 않도록 진행시킵니다. 그렇지 않으면 읽을 때 값이 사라지는 장치 register를 두 번 읽는 등의 문제가 생깁니다.

단계는 MMIO 요청과 결과 반영 순서입니다. data buffer → x2는 KVM이 응답을 게스트 register 상태에 반영하는 관계입니다. 예제의 주소·0xa5·0x12345678은 임의로 정한 값이며 실행 기록이 아닙니다.

io_mem_abort는 먼저 커널 안의 MMIO 처리 가능 여부를 확인합니다. 성공하면 커널 안에서 결과를 완료하고 계속 실행할 수 있습니다. 사용자 공간 처리가 필요한 경우에만 KVM_EXIT_MMIO를 준비합니다. 따라서 “MMIO는 항상 QEMU가 처리한다”도 맞지 않습니다. MMIO 해석·커널 처리·사용자 공간 반환

다시 KVM_RUN을 호출하면 arm.c의 시작 부분에서 kvm_handle_mmio_return을 먼저 처리합니다. 읽기 값의 폭과 확장 규칙을 적용하고 게스트 register에 반영한 뒤 PC를 조정합니다. KVM API 문서도 MMIO 같은 요청은 재진입하여 남은 처리를 끝내야 게스트 상태가 일관된다고 설명합니다. MMIO 완료를 먼저 처리하는 위치 KVM_RUN과 MMIO 완료 규칙

관찰한 현상을 어디에 연결해야 합니까?

관찰·질문먼저 나누어 볼 것
게스트에서 시스템 호출이 많습니다.게스트 EL0·EL1의 작업과 host guest-exit 횟수를 따로 봅니다. 호출 수가 곧 QEMU 복귀 횟수는 아닙니다.
Stage-2 fault가 반복됩니다.RAM의 첫 접근·쓰기 추적·권한 문제·MMIO·페이지 표 탐색 중 fault를 구분합니다.
VMM이 MMIO 응답을 썼는데 값이 반영되지 않았습니다.동일 vCPU의 KVM_RUN 재진입과 커널의 완료 처리까지 확인합니다.
KVM_RUN이 반환했습니다.ioctl의 반환값·errno부터 확인하고, 성공 반환에서 exit_reason과 대응하는 자료를 해석합니다.
실행 위치를 EL1이라고 들었습니다.게스트 커널인지 nVHE 호스트 커널인지, 현재 어떤 문맥이 적재되어 있는지 함께 확인합니다.

기존 KVM·Stage-2 글에서는 함수 연결과 매핑 코드를 이어서 읽으실 수 있습니다. 이 글의 숫자는 그 코드가 바꾸는 상태를 설명하기 위해 정한 예이며, 실제 KVM을 실행하거나 trace를 수집한 측정 결과는 아닙니다.