DUJINLABS.COM

Linux v6.18.37 · ARM64

코드를 읽기 전에 알아둘 개념

코드 한 줄을 읽을 때는 “어느 값을 읽고, 무엇을 바꾸며, 다음에는 어디로 가는가”를 차례로 설명해 보세요. 다음 예제의 주소와 숫자는 이해를 돕기 위한 가상 값입니다.

예제 1. 스택이 아래로 늘어난다는 뜻

stp x29, x30, [sp, #-16]!
mov x29, sp
ldp x29, x30, [sp], #16
ret

처음 SP가 0x1000이고 두 레지스터는 각각 8바이트라고 하겠습니다. 첫 줄은 SP를 0x0ff0으로 낮춘 뒤, x29를 0x0ff0에, x30을 0x0ff8에 저장합니다. 낮은 주소 쪽으로 공간을 확보했다는 뜻입니다. 종이에서 아래로 그렸기 때문에 주소가 작아지는 것은 아닙니다.

둘째 줄은 x29에 새 SP의 숫자를 복사하여 프레임 기준을 잡습니다. 셋째 줄은 보관한 두 값을 복구한 다음 SP를 0x1000으로 되돌립니다. 마지막 RET는 복원된 x30의 주소로 갑니다. 실제 함수에서는 둘째 줄과 셋째 줄 사이에 본래 작업이 들어갑니다.

예제 2. 비교 결과와 분기를 구분하기

cmp x0, #0
mov x1, #1
b.eq empty

CMP는 x0를 0으로 만드는 명령이 아닙니다. 둘이 같은지 계산하여 조건 플래그에 기록합니다. 중간 MOV는 이 플래그를 바꾸지 않으므로 B.EQ는 여전히 CMP의 결과를 봅니다. 같으면 empty라는 이름이 붙은 위치로, 다르면 다음 명령으로 갑니다.

예제 3. 매크로 정의와 함수 호출 구분하기

.macro keep_pair
    stp x0, x1, [sp, #-16]!
.endm

keep_pair

앞의 세 줄은 어셈블러에게 명령 묶음의 이름을 알려 줍니다. 마지막 keep_pair가 있는 자리에 저장 명령이 펼쳐집니다. BL로 다른 함수를 부르는 과정도, .endm에서 CPU가 반환하는 과정도 없습니다. SYM_INNER_LABEL 역시 도구에 위치 이름을 남기는 표기이며, 그 자체로 새 스택을 만들지 않습니다.

레지스터와 값

레지스터는 CPU가 지금 계산에 쓰는 작은 저장 공간입니다. 같은 x0라도 어느 순간에는 주소, 다음 순간에는 길이, 함수 호출 뒤에는 반환값이 들어갈 수 있습니다. 이름만으로 자료형이 정해지지 않습니다.

주소와 주소가 가리키는 내용

주소는 메모리 위치를 나타내는 숫자입니다. MOV로 주소를 복사하면 위치 숫자만 옮깁니다. LDR로 [주소]를 읽으면 그 위치에 보관된 값을 가져옵니다. 그 값이 다시 다른 주소일 수도 있습니다.

SP와 스택 공간

SP는 현재 사용 중인 스택의 경계를 나타냅니다. 이 코드의 스택은 공간을 확보할 때 낮은 주소로 이동합니다. 예를 들어 16바이트를 확보하면 SP가 0x1000에서 0x0ff0이 됩니다. 메모리를 새로 만들어 내는 것이 아니라 준비된 영역 안에서 사용할 부분을 정합니다.

가상 주소와 MMU

MMU는 CPU가 사용하는 가상 주소를 페이지 테이블에 따라 물리 주소로 변환합니다. 주소 변환을 켜거나 테이블을 바꿔도 다음 명령과 스택에 접근할 수 있어야 합니다. idmap은 같은 숫자의 VA와 PA를 연결해 전환 구간을 이어 주는 방법입니다.

예외 레벨과 복귀

예외가 들어오면 CPU는 정해진 벡터로 이동하고 복귀 PC와 PSTATE를 ELR·SPSR에 남깁니다. 커널은 일반 레지스터 등 추가 상태를 보존합니다. ERET는 이 복귀 정보를 사용하므로 주소만 바꾸는 RET와 역할이 다릅니다.

매크로와 실제 CPU 명령

매크로는 빌드할 때 여러 줄을 만들어 주는 규칙입니다. 함수 호출처럼 실행 중 다른 곳으로 갔다가 돌아오는 동작이 아닙니다. 인자 치환과 CONFIG 조건에 따라 명령 수와 동작이 달라집니다.

스택 프레임과 복귀 기록

함수나 예외 처리에서 보관할 값들을 스택의 한 구역에 둡니다. FP/LR 기록은 이전 호출을 찾고 복귀 주소를 유지하는 데 쓰입니다. 예외용 pt_regs는 일반 함수 프레임보다 더 넓은 실행 상태를 기록합니다.

비교 결과와 조건 분기

CMP나 TST는 계산 결과 대신 NZCV라는 조건 플래그를 남깁니다. 뒤의 B.EQ는 바로 앞 줄의 모양이 아니라 가장 최근에 플래그를 바꾼 명령의 결과를 봅니다. MOV·LDR처럼 플래그를 유지하는 명령이 사이에 올 수 있습니다.

호출 주소와 복귀 주소

BL/BLR은 이동하기 전에 다음 명령 주소를 LR에 기록합니다. 다른 함수를 또 호출하면 LR이 바뀌므로 원래 복귀 주소를 보관해야 합니다. B/BR은 새 복귀 주소를 만들지 않습니다.

호출 규약

서로 다른 코드가 인자, 결과, 보존할 레지스터를 어디에 둘지 합의한 규칙입니다. AAPCS64에서는 x0–x7을 주로 인자에 사용하고 x19–x29는 호출받은 쪽이 보존합니다. 펌웨어 호출에는 SMCCC 같은 추가 규약도 적용됩니다.

전처리·어셈블·링크·실행

전처리는 설정에 따라 소스를 선택하고, 어셈블러는 명령과 데이터를 바이트로 만들며, 링커는 조각을 배치하고 주소를 맞춥니다. CPU는 그 결과를 실행합니다. 소스에 적힌 줄이 어느 단계의 일인지 먼저 구분해야 합니다.

어셈블러 지시문

점으로 시작하는 .long, .section, .align 등은 도구에 주는 지시입니다. 데이터 출력, 배치 변경, 정렬을 수행합니다. CPU 레지스터를 즉시 바꾸는 실행 명령은 아닙니다.

정렬과 경계

주소를 일정 단위의 배수에 맞추는 일입니다. 16바이트 정렬 주소는 하위 4비트가 0입니다. 명령 슬롯, 페이지 테이블, 스택의 정렬은 서로 다른 요구사항이므로 단위를 함께 읽어야 합니다.

실행 상태의 저장과 복원

실행 상태는 어느 명령을 실행할지, 어떤 스택을 쓰는지, 레지스터에 어떤 값이 있는지를 포함합니다. 저장은 이를 메모리 기록으로 만들고, 복원은 기록을 CPU에 되돌리는 과정입니다. 목적에 따라 저장해야 할 범위가 다릅니다.

벡터 레지스터

여러 값을 한꺼번에 계산하는 레지스터입니다. FP/SIMD의 Q 레지스터는 128비트이고, SVE·SME는 가변 벡터 길이를 사용합니다. 그래서 저장 공간 크기를 항상 같은 숫자로 잡을 수 없습니다.

실행 코드의 패치

커널은 CPU 기능이나 추적 설정에 따라 특정 명령을 다른 명령으로 바꿀 수 있습니다. 원문 NOP가 실행 내내 NOP인 것은 아닙니다. 패치 메타데이터는 위치와 길이, 적용 조건을 연결합니다.

데이터 캐시와 명령어 캐시

메모리의 최신 내용과 CPU가 캐시에 갖고 있는 내용이 어긋날 수 있습니다. clean은 수정한 데이터를 필요한 지점까지 내보내고, invalidate는 캐시의 기존 사본을 더 이상 유효하게 쓰지 않게 합니다. 둘은 같은 작업이 아닙니다.

순서 보장과 완료 대기

DMB는 필요한 접근 순서를 보장하고 DSB는 지정 작업의 완료를 기다립니다. ISB는 변경된 실행 제어를 이후 명령에 반영합니다. 배리어 하나를 실행한다고 모든 캐시가 자동으로 비워지는 것은 아닙니다.

경쟁과 원자적 변경

두 CPU가 같은 값을 동시에 바꾸면 읽은 뒤 쓰는 사이에 값이 달라질 수 있습니다. 배타적 읽기·쓰기는 그 경쟁을 감지하고 실패 시 다시 시도하게 합니다. 비교 불일치와 저장 경쟁은 구분해야 합니다.

재배치와 상대 주소

주소가 아직 확정되지 않은 명령이나 데이터에는 링커가 나중에 채울 정보가 필요합니다. 절대 주소를 넣는 경우와 기준 위치에 대한 차이를 넣는 경우가 있으며, 각각 다른 계산과 값 범위를 사용합니다.

맨 위로 ↑