Linux v6.18.37 · AArch64 · 일반 데이터 접근과 부팅 시 주소 폭 선택

ARM64 주소 폭과 비정렬 접근을 구분해서 읽습니다

64비트 레지스터, 48·52비트 주소, 페이지 크기는 서로 다른 값입니다. 커널이 실제 주소 폭을 정하는 과정과 LDR이 읽는 바이트를 예제로 살펴봅니다.

64비트 레지스터가 64비트 주소 공간을 보장하지는 않습니다

x1에 숫자가 들어 있다고 그 숫자를 모두 유효한 메모리 주소로 사용할 수 있는 것은 아닙니다. CPU가 구현한 주소 변환 기능, 커널 설정, 현재 변환 제어 레지스터, 실제 페이지 테이블이 함께 맞아야 합니다. 주소 폭으로 표현할 수 있는 범위와 보드에 설치된 RAM 용량도 구분해야 합니다.

혼동하기 쉬운 부분
x0~x30의 64비트 폭AArch64 일반 레지스터에 담을 수 있는 비트 수입니다.64비트 숫자를 담는 것과 그 숫자로 접근할 수 있는 메모리가 존재하는 것은 다릅니다.
VA, Virtual Address프로그램이 사용하는 가상 주소입니다.커널과 사용자 영역의 크기, 페이지 테이블 단계는 설정과 실행 중인 CPU 기능에 영향을 받습니다.
IPA, Intermediate Physical AddressStage-2 변환을 사용하는 게스트에서 Stage-1의 출력 주소입니다.게스트가 물리 주소라고 사용하는 값도 호스트 RAM의 PA와 같다고 가정할 수 없습니다.
PA, Physical Address주소 변환 결과가 가리키는 물리 주소입니다.CPU의 주소 폭 안에도 RAM, 장치 영역, 사용할 수 없는 구간이 함께 있을 수 있습니다.
페이지 크기이 글에서는 4KB·16KB·64KB translation granule을 구분합니다.페이지 한 개의 크기를 주소 전체의 폭으로 읽으면 안 됩니다.

“ARMv8-A의 PA는 최대 48비트”라는 말은 이후의 확장까지 포함한 설명으로는 부족합니다. ARMv8.2의 LPA는 64KB granule에서 52비트 PA·IPA를, LVA는 52비트 VA를 지원하는 기능입니다. 이후 LPA2를 사용하는 4KB·16KB 구성도 있습니다. 기능 이름이 비슷해도 VA와 PA 설정을 같은 것으로 취급하지 않습니다. 아래 내용은 가능한 모든 최신 확장을 나열하는 대신 Linux v6.18.37의 설정과 부팅 경로를 기준으로 설명합니다. ARM64 주소 폭·페이지 크기 설정

주소로 메모리를 읽기까지 확인하는 조건
  1. 명령이 x1을 주소로 사용합니다.

    x1은 64비트 값을 담습니다. 그중 어떤 비트를 주소 변환에 쓰는지는 현재 설정을 따릅니다.

    현재 주소 변환 설정을 적용합니다.

  2. 허용된 VA 범위와 페이지 테이블을 확인합니다.

    주소 폭에 들어오더라도 매핑이 없거나 읽기 권한이 없으면 접근이 끝나지 않습니다.

    변환 결과와 메모리 속성을 얻습니다.

  3. 물리 대상과 접근 조건을 확인합니다.

    RAM인지 Device 영역인지, 접근 크기와 정렬 조건이 맞는지도 중요합니다. 게스트라면 Stage-2 조건을 추가로 봅니다.

화살표는 주소 접근의 조건을 이해하기 위한 논리적 순서입니다. 매 명령마다 이 순서로 페이지 테이블을 메모리에서 다시 읽는다는 뜻은 아니며, TLB가 변환 결과를 보관할 수 있습니다.

빌드 시 선택과 부팅 중 판정을 나누어 봅니다

이름정하는 시점과 역할그 값만으로 알 수 없는 것
CONFIG_ARM64_VA_BITS_52커널 빌드에서 52비트 VA 지원 구성을 선택합니다. CONFIG_ARM64_VA_BITS의 값은 52가 됩니다.이번 CPU에서 실제로 52비트 VA를 사용할 수 있는지는 부팅 때 판정합니다.
CONFIG_ARM64_PA_BITS_52커널이 지원할 최대 PA 범위를 선택합니다.실제 CPU의 PA 능력이나 설치된 RAM 크기를 52비트로 늘려 주지 않습니다.
CONFIG_ARM64_4K_PAGES / 16K_PAGES / 64K_PAGES페이지 크기 구성을 선택합니다.같은 52비트 지원이라도 필요한 기능과 페이지 테이블 형식이 달라집니다.
CONFIG_ARM64_LPA2이 버전에서 PA_BITS_52가 켜지고 64K_PAGES가 아니면 선택되는 내부 설정입니다.CPU의 LPA2 지원을 강제로 만들어 주는 설정이 아닙니다.
VA_BITS_MIN해당 빌드가 사용할 최소 VA 폭을 소스에서 계산한 상수입니다.항상 48인 상수가 아닙니다. VA_BITS가 52인 16KB 구성에서는 47입니다.
vabits_actual이 버전의 VA_BITS가 48보다 큰 빌드에서는 TCR_EL1의 T1SZ를 읽어 실제 커널 VA 폭을 구하는 매크로입니다.별도의 고정된 52비트 상수나 RAM 용량을 뜻하지 않습니다.

CONFIG 이름은 빌드에 포함할 코드와 상수를 정합니다. IS_ENABLED(CONFIG_ARM64_LPA2)도 이 빌드의 설정을 판정하는 표현입니다. 반면 early_map_kernel의 cpu_has_lpa2()와 cpu_has_lva()는 부팅 CPU의 기능 판정입니다. 지원되지 않는 경우 va_bits를 VA_BITS_MIN으로 낮추는 분기가 실제 실행 중에 일어납니다. early_map_kernel VA_BITS_MIN·vabits_actual

cpu_has_lpa2는 ID_AA64MMFR0_EL1에서 현재 페이지 크기에 해당하는 TGran 필드를 읽어 판정합니다. cpu_has_lva는 ID_AA64MMFR2_EL1의 VARange 필드를 확인합니다. 두 helper 모두 커널의 feature override를 적용한 값을 사용하므로 아래의 “사용 가능”은 이 부팅 경로에서의 판정 결과를 뜻합니다. MMFR은 CPU가 제공하는 메모리 모델 기능을 나타내는 식별 레지스터이며 현재 task의 페이지 테이블 주소를 담는 TTBR과 역할이 다릅니다. cpu_has_lva·cpu_has_lpa2

Kconfig의 조합도 확인해야 합니다. 4KB·16KB에서 VA_BITS_52를 선택하면 PA_BITS_48의 조건을 만족하지 못하고 PA_BITS_52를 사용합니다. 64KB에서는 이 두 설정의 선택 조건이 다릅니다. 따라서 “VA를 52로 골랐으니 PA도 언제나 같은 이유로 52다”라고 모든 페이지 크기에 적용하지 않습니다. ARM64_PA_BITS_48·ARM64_PA_BITS_52·ARM64_LPA2

52비트로 빌드했는데 왜 47비트로 동작합니까?

CONFIG_ARM64_VA_BITS_52=y인 커널의 부팅 경로를 비교하겠습니다. 표는 VA 폭을 선택하는 동작이며, 그 밖의 CPU·부팅 요구 사항도 만족한다고 가정합니다. 주소 폭을 줄이는 처리가 모든 다른 호환성 문제까지 해결한다는 뜻은 아닙니다.

페이지 크기52비트 VA를 사용할 조건해당 기능이 없을 때의 VA_BITS_MIN
4KB이 경로의 cpu_has_lpa2()가 참이어야 합니다.48비트
16KB이 경로의 cpu_has_lpa2()가 참이어야 합니다.47비트
64KB이 경로의 cpu_has_lva()가 참이어야 합니다.48비트
16KB·52비트 빌드의 부팅 시 선택

1. 이미지에는 52비트 지원이 들어 있습니다.

이 예에서는 VA_BITS=52, VA_BITS_MIN=47입니다. 이 값들은 빌드 구성으로 정해집니다. 아직 실제 CPU에서 어느 쪽을 사용할지의 결과는 아닙니다.

2. early_map_kernel이 CPU 기능을 확인합니다.

LPA2 기능 판정이 거짓이면 va_bits=VA_BITS_MIN으로 바꾸고 root_level을 하나 올립니다. root_level은 초기 페이지 테이블의 시작 단계를 나타내며 CPU의 EL 번호가 아닙니다. 지원되는 경우에는 va_bits=52를 유지합니다.

3. 사용할 T1SZ와 페이지 테이블이 달라집니다.

CPU 기능 판정커널 VA 폭TCR_EL1.T1SZ 필드 값
이 경로에서 LPA2 사용 가능5264−52 = 12
이 경로에서 LPA2 사용 불가4764−47 = 17

52비트를 사용하는 경로는 T1SZ를 갱신하고 LPA2 형식에 맞춘 초기 매핑 전환도 수행합니다. 47비트 경로는 앞서 준비된 최소 폭 설정을 사용합니다. 숫자 12·17은 TCR_EL1 전체 값이 아니라 T1SZ 필드의 값입니다.

4. vabits_actual로 현재 커널 VA 폭을 얻습니다.

VA_BITS>48인 이 빌드에서 식은 64 − ((read_tcr() >> 16) & 63)입니다. 16만큼 이동해 T1SZ 위치를 맞추고 63, 즉 0x3f로 6비트를 추출합니다. T1SZ가 17이면 결과는 47입니다. 설정 상수 VA_BITS는 여전히 52로 남습니다.

단계는 빌드 설정을 읽는 시점부터 부팅 결과를 해석하는 순서입니다. 두 CPU 조건을 비교한 설명용 예이며 실제 기기에서 측정한 로그가 아닙니다. 주소 폭이 낮아져도 x 레지스터의 폭은 64비트입니다.

이 결과는 v6.18.37의 memory.h와 early_map_kernel을 함께 읽어 얻었습니다. 같은 태그의 개요 문서나 Kconfig 도움말에 있는 “48비트로 되돌아간다”는 간략한 표현만으로 16KB 구성까지 판단하면 47비트 조건을 놓칩니다. T1SZ의 비트 위치와 64−VA_BITS 계산은 pgtable-hwdef.h에서도 확인할 수 있습니다. 페이지 크기별 최소 VA 폭 기능 확인·T1SZ 변경·LPA2 초기 매핑 TCR_T1SZ·OFFSET·MASK

사용자 공간도 별도로 봅니다. 52비트 VA를 지원한다고 기존 프로그램의 모든 mmap 결과가 자동으로 높은 주소에 배치되는 것은 아닙니다. 일반 구성은 호환성을 위해 낮은 주소를 우선하고, 넓은 주소 범위를 요청하는 hint를 처리합니다. CONFIG_ARM64_FORCE_52BIT는 이 호환 동작을 바꾸는 시험용 옵션이므로 일반 운영 설정으로 권하는 항목이 아닙니다. 52비트 사용자 주소·FORCE_52BIT 도움말

주소의 최상위 비트 하나만 보고 사용자·커널을 나누지 않습니다

주소 그림에서 사용자 공간은 0으로, 커널 공간은 f로 시작하는 예를 자주 봅니다. 하지만 tagged pointer까지 포함해 “bit 63이 1이면 무조건 커널 주소”라고 설명하면 틀립니다. Linux의 AArch64 사용자 데이터 접근에는 상위 바이트를 주소 변환에서 무시하는 TBI0, Top Byte Ignore 설정이 사용됩니다.

예제 포인터주소 변환에서 사용하는 사용자 주소주의할 조건
0x00000000000120000x0000000000012000해당 사용자 메모리가 매핑되어 있고 접근 권한이 있다고 가정합니다.
0xab000000000120000x0000000000012000상위 바이트 0xab를 무시하는 데이터 접근의 예입니다. bit 63이 1이어도 이것만으로 커널 주소라고 판정하지 않습니다.

이 예는 MTE tag 검사나 그 밖의 접근 제한이 걸리지 않는 Normal memory를 가정합니다. TBI가 동작해도 x1 안의 비트가 자동으로 0x0000000000012000으로 다시 쓰이는 것은 아닙니다. 주소 변환에 상위 바이트를 사용하지 않는 동작과 레지스터 값을 바꾸는 동작을 구분해야 합니다. Tagged virtual addresses

CPU가 tagged pointer로 읽을 수 있는 것과 그 포인터를 모든 시스템 호출에 그대로 넘길 수 있는 것은 별개의 규칙입니다. 커널이 사용자 데이터를 읽는 write 같은 호출에는 기본적으로 꺼져 있는 tagged address ABI의 활성화 조건이 있습니다. prctl(PR_SET_TAGGED_ADDR_CTRL, PR_TAGGED_ADDR_ENABLE, 0, 0, 0)로 요청하며 실패 여부도 확인합니다. 활성화한 뒤에도 mmap, brk, mremap의 new_address 등은 예외 규칙이 있으므로 모든 포인터의 tag가 허용된다고 일반화하지 않습니다. Tagged Address ABI의 조건과 예외

비정렬 LDR이 항상 예외를 일으키는 것은 아닙니다

4바이트 접근의 시작 주소가 4의 배수가 아니면 자연 정렬을 만족하지 않습니다. 0x10000에서 4바이트를 읽는 경우는 정렬되어 있고, 0x10001에서 읽는 경우는 비정렬입니다. 정렬되지 않았다는 사실과 CPU가 그 접근을 허용하는지는 구분해야 합니다. 자연 정렬과 이식성

일반 LDR·STR의 데이터 접근 조건비정렬 접근의 결과
Normal memory, 적용되는 SCTLR_ELx.A=0비정렬이라는 이유만으로 alignment fault가 나지는 않습니다. 매핑·권한 등 다른 접근 조건은 여전히 만족해야 합니다.
Normal memory, 적용되는 SCTLR_ELx.A=1정렬 검사를 켠 경우에는 alignment fault의 대상입니다.
Device memory의 비정렬 접근일반적인 데이터 접근에서도 alignment fault가 발생합니다. MMIO를 RAM처럼 읽으면 안 됩니다.

A는 alignment check 제어 비트입니다. 이 표는 일반적인 LDR·STR 설명 범위이며 exclusive·acquire/release·atomic·SIMD 명령의 세부 조건까지 같은 표로 판정하지 않습니다. LDP·STP의 데이터 정렬은 두 레지스터의 총 크기가 아니라 각 요소 크기를 기준으로 보며, SP를 주소로 사용하는 경우의 별도 정렬 규칙도 구분해야 합니다. Armv8-A memory model guide · 12절 정렬

비정렬 접근이 허용되는 CPU에서도 C의 u8 포인터를 임의로 u32 포인터로 바꿔 역참조하는 습관은 이식성 문제를 만듭니다. 컴파일러의 타입·정렬 가정과 CPU 명령의 실행 가능 여부는 같은 조건이 아닙니다. 장치 register에는 장치가 정한 접근 폭과 readl/writel 계열의 I/O 접근 규칙도 필요합니다. 타입 변환·비정렬 helper 장치 I/O 접근 규칙

LDR 한 줄로 x0와 x1이 어떻게 바뀝니까?

다음은 little-endian 데이터 접근의 계산 예입니다. x1=0x0000000000010001이고, 그 주소부터 네 바이트가 11 22 33 44라고 정하겠습니다. Normal memory, 적용되는 A=0이며 매핑·권한·tag 문제와 동시 쓰기가 없다고 가정합니다. x0의 시작 값 0xdeadbeef12345678은 변화가 보이도록 선택한 임의의 숫자입니다.

ldr w0, [x1]
메모리 네 바이트를 읽은 뒤의 레지스터

1. 실행 직전

대상
x1: 읽을 주소0x0000000000010001
x0: 읽기 결과를 넣을 레지스터0xdeadbeef12345678
주소 0x10001 / 0x10002 / 0x10003 / 0x100040x11 / 0x22 / 0x33 / 0x44

시작 주소는 4의 배수가 아니지만 위의 접근 조건에서는 허용됩니다.

2. 네 바이트를 32비트 값으로 해석합니다.

little-endian에서 낮은 주소의 0x11이 값의 하위 바이트가 됩니다. 계산 결과는 0x44332211입니다. x1의 숫자 0x10001 자체를 w0로 복사하는 명령이 아닙니다.

3. 실행 직후

대상결과
w00x44332211
x00x0000000044332211
x10x0000000000010001: 그대로입니다.
읽은 메모리0x11 / 0x22 / 0x33 / 0x44: 그대로입니다.

w0는 x0의 하위 32비트이며, w0에 쓰면 x0의 상위 32비트는 0이 됩니다. 이 주소 형식에는 writeback이 없으므로 x1도 증가하지 않습니다.

단계는 명령의 입력·해석·결과를 보여 줍니다. 메모리 → w0는 읽은 데이터의 전달 방향이며, x1을 옮기거나 메모리 데이터를 지우는 뜻이 아닙니다. 파이프라인 사이클이나 실기 실행 기록을 나타내지 않습니다.

대괄호는 x1의 값이 가리키는 메모리를 읽는다는 표시입니다. 위 예의 결과는 접근이 성공했을 때의 상태이며, fault 경로의 레지스터 상태를 이 성공 결과로 표시하면 안 됩니다. A64 ISA guide 1.2 · W/X 관계와 load 주소 형식

커널에서 바이트 버퍼를 해석할 때는 의도를 코드에 적습니다

수신한 형식이 “첫 바이트는 종류, 뒤의 네 바이트는 little-endian 정수”라고 정해진 예입니다. buf는 커널이 읽을 수 있는 일반 메모리이며, len은 실제 유효 바이트 수, out은 유효한 출력 포인터라고 가정합니다. 장치가 아직 쓰고 있는 DMA 버퍼나 사용자 포인터를 그대로 받은 상태는 이 전제에 포함되지 않습니다.

#include <linux/errno.h>
#include <linux/unaligned.h>

static int read_payload_value(const u8 *buf, size_t len, u32 *out)
{
    if (len < 5)
        return -EINVAL;

    *out = get_unaligned_le32(buf + 1);
    return 0;
}
버퍼의 배치와 helper가 맡는 일
  1. len이 5 이상인지 확인합니다.

    부족하면 -EINVAL을 반환하고 out에 쓰지 않습니다. EINVAL은 잘못된 인자를 나타내는 오류 번호이며, 커널 함수는 그 음수를 반환하는 규약을 사용합니다.

    필요한 바이트가 있을 때만 뒤의 필드로 이동합니다.

  2. buf + 1에서 네 바이트를 읽습니다.

    u8은 한 바이트이므로 +1은 다음 바이트의 주소입니다. 예를 들어 buf가 0x10000이면 필드는 0x10001에서 시작합니다.

    정렬되지 않을 수 있는 little-endian 필드를 숫자로 해석합니다.

  3. get_unaligned_le32의 결과를 *out에 씁니다.

    11 22 33 44는 CPU endian에 관계없이 수치 0x44332211로 해석합니다. 입력 버퍼를 재정렬하거나 그 바이트를 수정하지 않습니다.

화살표는 길이 검사 → 필드 읽기 → 출력 값 저장의 실행 순서입니다. 예제 숫자는 버퍼 형식을 설명하기 위한 값이며 실제 패킷을 캡처한 결과가 아닙니다.

get_unaligned_le32의 le는 little-endian, 32는 읽을 값의 비트 수입니다. 이 helper는 정렬 조건을 알리고 endian을 변환하지만, 길이 검사·사용자 주소 검증·DMA 완료 확인·잠금은 수행하지 않습니다. 내부 구현은 컴파일러가 비정렬 배치를 알 수 있게 하며, 실제 기계 명령이 언제나 네 번의 1바이트 load로 나온다고 단정하지 않습니다. get_unaligned_le32 구현 helper 사용과 비용

동시에 다른 CPU나 장치가 같은 필드를 바꾼다면 별도의 소유권·동기화 규칙이 필요합니다. helper를 사용했다는 이유로 읽기가 원자적이거나 최신 값이라는 보장은 생기지 않습니다. 기존 DMA 소유권 글과 저장 순서 글에서 이 부분을 이어서 설명합니다.

fault가 나면 주소 폭·매핑·권한·정렬을 나누어 봅니다

관찰한 문제확인할 내용
주소가 넓어 보입니다.tag를 포함한 값인지, 실행 중인 VA 폭이 몇 비트인지 확인합니다. CONFIG 숫자만으로 판단하지 않습니다.
translation fault입니다.현재 주소 공간에서 필요한 변환 entry가 있는지와 fault 단계·level을 봅니다. 게스트라면 Stage-1과 Stage-2를 구분합니다.
permission fault입니다.읽기·쓰기·실행 권한과 현재 실행 조건을 확인합니다. 해당 주소가 있다는 사실만으로 접근 권한이 생기지 않습니다.
alignment fault입니다.주소, 접근 크기, 명령 종류, 메모리 속성과 적용되는 정렬 검사 설정을 확인합니다. 페이지를 더 할당하는 처리로 일반화하지 않습니다.

Linux v6.18.37의 fault table은 데이터 alignment fault를 SIGBUS/BUS_ADRALN에 연결합니다. 네이티브 AArch64 사용자 접근에서 이 경로를 타면 do_bad_area가 그 신호를 전달하는 경로로 갑니다. 커널 모드 fault는 같은 사용자 신호 처리로 설명하지 않습니다. CONFIG_COMPAT_ALIGNMENT_FIXUPS가 켜져 있더라도 do_compat_alignment_fixup은 compat_user_mode 조건의 AArch32 사용자 접근에 대한 별도 경로이며, 모든 AArch64 비정렬 명령을 자동 복구하는 옵션이 아닙니다. do_alignment_fault·do_bad_area·fault_info

기존 페이지 fault 글은 VMA를 찾고 fault를 처리하는 경로를, 페이지 테이블 글은 주소와 속성 비트의 배치를 설명합니다. 여기서는 그 앞에서 구분해야 하는 주소 폭·tag·정렬 조건과 명령의 입력·출력 값을 보충했습니다. 예제는 계산으로 확인한 설명이며 실제 장치에서 실행한 결과는 아닙니다.