← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 20/21

Kernel module relocation과 instruction range

ELF module의 section을 vmalloc 영역에 배치하고 architecture relocation을 적용해 symbol address와 instruction immediate를 고치는 방법을 비교합니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
파일 3개 · 원문 277줄
기준 태그
Linux v6.18.37
분석 축
상태 · 접근 순서 · 객체 수명 · 지연 시간

01 · QUESTION

무엇을 확인할 것인가

같은 R_*_CALL relocation이라도 target이 branch range를 벗어날 때 왜 PLT, thunk, trampoline이 필요하며 text를 쓴 뒤 instruction cache를 어떻게 동기화하는가?

module loader가 ELF section, symbol과 relocation table을 검증하고 core/init layout에 memory를 할당한 뒤 architecture apply_relocate_add()가 S+A-P 등의 식으로 location을 patch한다. 이후 permission을 RX/RO/RW로 강화한다.

untrusted module ELF parsing, writable text patch 구간, instruction-cache publication과 module live state 전환을 분리한다. relocation 실패 시 module init을 호출하기 전에 모든 allocation을 unwind해야 한다.

지연 시간 관점load 시간은 section allocation, relocation 수, symbol lookup, signature/ORC validation과 icache synchronization에 비례한다. runtime hot path에는 직접 branch 또는 PLT indirection 비용이 남는다.

02 · CONTRACT

공통 규약과 아키텍처별 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64ADRP/ADD, CALL26/JUMP26와 module PLTimmediate alignment/range overflow, BTI/PAC landing 요구 또는 stale icache는 load 실패와 이전 instruction 실행을 만든다.relocation type, S/A/P 값, encoded immediate, PLT address, module text range와 icache flush를 확인한다.
x86-64R_X86_64_64, PC32/PLT32와 alternativesrel32 거리 초과, CET IBT ENDBR 누락 또는 metadata 불일치는 잘못된 branch target, control-protection fault와 잘못된 unwind를 만들 수 있다. PC32/PLT32 경로에는 PLT fallback이 없다.relocation type, 64-bit result 또는 s32 displacement, P/S/A, module region, ENDBR와 ORC table을 확인한다.
RISC-VHI20/LO12 pair, CALL_PLT, JAL/BRANCH와 relaxationHI20 rounding과 LO12 sign 처리, paired relocation 위치 또는 compressed instruction width 오류가 완전히 다른 target을 만든다.relocation pair, S/A/P, HI20 rounding, LO12 sign extension, PLT entry와 remote icache synchronization을 본다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

그림 1. 같은 기능을 구현하는 서로 다른 방식각 명령어 집합(ISA)에서 추적할 상태와 확인할 항목을 나란히 정리했습니다.

arm64

mechanism
ADRP/ADD, CALL26/JUMP26와 module PLT
state
AArch64 relocation은 instruction bitfield에 page-relative 또는 PC-relative immediate를 encode한다. ±128MiB branch range를 벗어나면 module PLT entry가 absolute target을 load해 branch한다.
checkpoint
relocation type, S/A/P 값, encoded immediate, PLT address, module text range와 icache flush를 확인한다.

x86-64

mechanism
R_X86_64_64, PC32/PLT32와 alternatives
state
absolute 64-bit와 signed 32-bit PC-relative relocation이 중심이다. module을 kernel text와 가까운 범위에 배치해 rel32 call/jump의 ±2GiB 제한을 맞춘다. 이 x86-64 relocation 함수에는 거리 초과 시 PLT를 만드는 fallback이 없다.
checkpoint
relocation type, 64-bit result 또는 s32 displacement, P/S/A, module region, ENDBR와 ORC table을 확인한다.

RISC-V

mechanism
HI20/LO12 pair, CALL_PLT, JAL/BRANCH와 relaxation
state
AUIPC+JALR 또는 LUI+ADDI처럼 여러 instruction이 하나의 symbol address를 구성하므로 paired relocation을 일관되게 적용한다. 먼 target에는 toolchain이 AUIPC+JALR의 CALL 또는 CALL_PLT relocation을 내야 한다. JAL relocation 자체는 PLT로 자동 변환되지 않는다.
checkpoint
relocation pair, S/A/P, HI20 rounding, LO12 sign extension, PLT entry와 remote icache synchronization을 본다.
그림 2. 공통 커널 코드와 아키텍처별 함수의 역할공통 커널 코드와 아키텍처별 코드가 각각 담당하는 일을 구분했습니다.
리눅스 공통 규약module loader가 ELF section, symbol과 relocation table을 검증하고 core/init layout에 memory를 할당한 뒤 architecture apply_relocate_add()가 S+A-P 등의 식으로 location을 patch한다. 이후 permission을 RX/RO/RW로 강화한다.
arm64ADRP/ADD, CALL26/JUMP26와 module PLTtext patch를 완료한 뒤 D-cache clean과 I-cache invalidate를 수행하고 permission을 executable/read-only로 바꾼다.
x86-64R_X86_64_64, PC32/PLT32와 alternatives초기 module relocation은 memcpy로 기록하고, 이미 형성된 module에 대한 livepatch relocation은 text_poke와 CPU별 동기화를 사용한다. ORC/ftrace metadata도 relocated address에 맞춰 등록한다.
RISC-VHI20/LO12 pair, CALL_PLT, JAL/BRANCH와 relaxationinstruction pair patch와 icache sync가 끝난 뒤 모든 hart가 새 code를 보도록 remote fence.i 경로를 사용한다.
객체 수명 관리module text/data와 symbol/unwind/ftrace metadata는 refcount와 RCU grace period가 끝날 때까지 유지된다. 실행 중인 CPU의 return address가 module text를 가리키면 unload할 수 없다.
그림 3. 분기 범위를 벗어났을 때의 서로 다른 처리같은 거리 초과라도 relocation type과 ISA에 따라 우회, 오류, 또는 별도 명령 형식이 됩니다.
arm64CALL26/JUMP26: ±128 MiB branch 검사→-ERANGE이면 module PLT entry 생성→PLT까지의 branch를 다시 encode; 실패하면 load 중단
x86-64PC32/PLT32: S + A - P 계산→4-byte displacement를 직접 기록→이 relocation 경로에는 arm64식 PLT fallback이 없음
RISC-VCALL_PLT: AUIPC+JALR 거리 검사→RV64 유효 범위 밖이면 CONFIG_MODULE_SECTIONS에서 PLT 생성→일반 CALL은 범위 초과 오류; JAL relocation은 PLT로 자동 변환하지 않음

RV64 AUIPC+JALR의 정확한 상대 거리 범위는 [-2^31 - 2^11, 2^31 - 2^11)이다. RV32의 이 helper는 별도 범위 검사를 하지 않는다. JAL relocation은 즉시값 비트를 기록하며 여기서 긴 호출로 바꾸거나 범위 오류를 별도로 보고하지 않는다.

그림 3 코드 근거: arm64 · x86-64 · RISC-V

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

arm64 · Linux 6.18.37

ADRP/ADD, CALL26/JUMP26와 module PLT

AArch64 relocation은 instruction bitfield에 page-relative 또는 PC-relative immediate를 encode한다. ±128MiB branch range를 벗어나면 module PLT entry가 absolute target을 load해 branch한다.

원본 코드: arch/arm64/kernel/module.c:223-315

223		insn = aarch64_insn_gen_branch_imm((u64)place, val,
224						   AARCH64_INSN_BRANCH_NOLINK);
225	}
226 
227	WRITE_PLACE(place, cpu_to_le32(insn), me);
228	return 0;
229}
230 
231int apply_relocate_add(Elf64_Shdr *sechdrs,
232		       const char *strtab,
233		       unsigned int symindex,
234		       unsigned int relsec,
235		       struct module *me)
236{
237	unsigned int i;
238	int ovf;
239	bool overflow_check;
240	Elf64_Sym *sym;
241	void *loc;
242	u64 val;
243	Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr;
244 
245	for (i = 0; i < sechdrs[relsec].sh_size / sizeof(*rel); i++) {
246		/* loc corresponds to P in the AArch64 ELF document. */
247		loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr
248			+ rel[i].r_offset;
249 
250		/* sym is the ELF symbol we're referring to. */
251		sym = (Elf64_Sym *)sechdrs[symindex].sh_addr
252			+ ELF64_R_SYM(rel[i].r_info);
253 
254		/* val corresponds to (S + A) in the AArch64 ELF document. */
255		val = sym->st_value + rel[i].r_addend;
256 
257		/* Check for overflow by default. */
258		overflow_check = true;
259 
260		/* Perform the static relocation. */
261		switch (ELF64_R_TYPE(rel[i].r_info)) {
262		/* Null relocations. */
263		case R_ARM_NONE:
264		case R_AARCH64_NONE:
265			ovf = 0;
266			break;
267 
268		/* Data relocations. */
269		case R_AARCH64_ABS64:
270			overflow_check = false;
271			ovf = reloc_data(RELOC_OP_ABS, loc, val, 64, me);
272			break;
273		case R_AARCH64_ABS32:
274			ovf = reloc_data(RELOC_OP_ABS, loc, val, 32, me);
275			break;
276		case R_AARCH64_ABS16:
277			ovf = reloc_data(RELOC_OP_ABS, loc, val, 16, me);
278			break;
279		case R_AARCH64_PREL64:
280			overflow_check = false;
281			ovf = reloc_data(RELOC_OP_PREL, loc, val, 64, me);
282			break;
283		case R_AARCH64_PREL32:
284			ovf = reloc_data(RELOC_OP_PREL, loc, val, 32, me);
285			break;
286		case R_AARCH64_PREL16:
287			ovf = reloc_data(RELOC_OP_PREL, loc, val, 16, me);
288			break;
289 
290		/* MOVW instruction relocations. */
291		case R_AARCH64_MOVW_UABS_G0_NC:
292			overflow_check = false;
293			fallthrough;
294		case R_AARCH64_MOVW_UABS_G0:
295			ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 0,
296					      AARCH64_INSN_IMM_MOVKZ, me);
297			break;
298		case R_AARCH64_MOVW_UABS_G1_NC:
299			overflow_check = false;
300			fallthrough;
301		case R_AARCH64_MOVW_UABS_G1:
302			ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 16,
303					      AARCH64_INSN_IMM_MOVKZ, me);
304			break;
305		case R_AARCH64_MOVW_UABS_G2_NC:
306			overflow_check = false;
307			fallthrough;
308		case R_AARCH64_MOVW_UABS_G2:
309			ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 32,
310					      AARCH64_INSN_IMM_MOVKZ, me);
311			break;
312		case R_AARCH64_MOVW_UABS_G3:
313			/* We're using the top bits so we can't overflow. */
314			overflow_check = false;
315			ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 48,

주요 구문 해설

원본 93줄은 그대로 표시하며, 검토한 72개 구문에 설명을 붙였습니다.

L223 insn = aarch64_insn_gen_branch_imm((u64)place, val,

ADRP를 ADR로 바꿔도 목적지에 닿지 않으면, 원래 명령 위치에서 보조 코드인 veneer로 가는 즉시 분기 명령을 만듭니다.

L224 AARCH64_INSN_BRANCH_NOLINK);

복귀 주소를 LR에 남기지 않는 B 형태의 분기를 선택합니다. 원래 ADRP도 함수 호출이 아니므로 veneer로 이동하면서 LR을 덮어서는 안 됩니다.

L227 WRITE_PLACE(place, cpu_to_le32(insn), me);

재배치 결과로 만든 32비트 명령을 little-endian 형식으로 대상 위치에 씁니다. 아직 형성 중인 모듈은 직접 쓰고, 이미 형성된 모듈에는 명령 복사 helper를 사용하므로 같은 매크로라도 모듈 상태에 따라 쓰기 방식이 달라집니다.

L228 return 0;

ADRP 대체 명령을 해당 위치에 기록했으므로 재배치 성공을 0으로 반환합니다.

L231int apply_relocate_add(Elf64_Shdr *sechdrs,

arm64 ELF의 RELA 재배치 섹션을 모듈의 실제 적재 주소에 맞춰 적용하는 함수입니다. sechdrs는 섹션들의 주소·크기·연결 정보를 제공합니다.

L232 const char *strtab,

심벌 이름을 담은 문자열 표를 받는 인자입니다. 주소를 구할 수 없거나 재배치가 잘못됐을 때 관련 이름을 식별할 수 있습니다.

L233 unsigned int symindex,

재배치가 참조할 심벌 표 섹션의 인덱스입니다. r_info의 심벌 번호는 이 표 안에서 해석합니다.

L234 unsigned int relsec,

이번에 처리할 RELA 섹션의 인덱스입니다. 이 섹션의 sh_info는 실제로 수정할 대상 섹션을 지정합니다.

L235 struct module *me)

재배치를 적용할 모듈 자료입니다. 모듈 이름·메모리·형성 상태를 제공하며 이미 형성된 코드의 쓰기 방식에도 영향을 줍니다.

L237 unsigned int i;

RELA 섹션 안에서 현재 처리 중인 재배치 항목의 인덱스입니다.

L238 int ovf;

선택한 재배치 helper의 성공 또는 범위 초과 결과를 받습니다. 아래 공통 오류 검사에서 이번 항목을 수용할 수 있는지 판단합니다.

L239 bool overflow_check;

이번 종류에서 표현 범위 초과를 오류로 볼지 나타냅니다. 기본은 검사이며 NC 종류와 전체 64비트 기록에서는 검사를 생략합니다.

L240 Elf64_Sym *sym;

현재 재배치가 가리키는 ELF 심벌 항목의 포인터입니다. 이 항목의 st_value가 주소 계산의 심벌 값 S가 됩니다.

L241 void *loc;

명령이나 데이터를 실제로 고칠 위치를 저장합니다. ELF 재배치 식에서 현재 위치 P에 해당합니다.

L242 u64 val;

심벌 값 S와 해당 RELA의 addend A를 합한 64비트 값을 저장합니다. 종류에 따라 그대로 쓰거나 위치 P를 빼거나 일부 비트를 추출합니다.

L243 Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr;

지정한 재배치 섹션의 메모리 주소를 Elf64_Rela 배열로 해석합니다. 각 항목은 위치·심벌과 종류·별도 addend를 담습니다.

L245 for (i = 0; i < sechdrs[relsec].sh_size / sizeof(*rel); i++) {

각 RELA entry를 순회하며 symbol, addend와 patch location을 계산한다.

L247 loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr

재배치 섹션의 sh_info가 가리키는 대상 섹션의 실제 적재 주소를 가져옵니다. 수정 위치 P의 기준점입니다.

L248 + rel[i].r_offset;

대상 섹션 시작에 이번 항목의 바이트 오프셋을 더해 수정할 정확한 위치를 구합니다.

L251 sym = (Elf64_Sym *)sechdrs[symindex].sh_addr

심벌 표 섹션의 적재 주소를 Elf64_Sym 배열의 시작으로 해석합니다. 다음 줄이 이번 재배치의 심벌 인덱스를 더합니다.

L252 + ELF64_R_SYM(rel[i].r_info);

재배치 정보 r_info에서 심벌 표의 인덱스를 뽑아 표의 시작 주소에 더합니다. 그 결과 sym은 이번 재배치가 참조하는 심벌을 가리키며, 다음에 심벌 값과 addend를 더해 최종 값을 계산합니다.

L255 val = sym->st_value + rel[i].r_addend;

심벌의 실제 값에 RELA 항목이 가진 addend를 더해 S+A를 만듭니다. 재배치 종류별 계산은 이 공통 값을 입력으로 사용합니다.

L258 overflow_check = true;

새 재배치 항목마다 범위 검사를 기본적으로 켭니다. 앞 항목이 NC였더라도 그 설정이 다음 항목에 남지 않게 합니다.

L261 switch (ELF64_R_TYPE(rel[i].r_info)) {

ABS, PREL, CALL/JUMP, ADRP 등 AArch64 relocation별 encoding을 선택한다.

L263 case R_ARM_NONE:

기존 Arm의 no-op 재배치 번호도 아무것도 수정하지 않는 종류로 받아들입니다. 다음 AArch64 NONE과 같은 본문을 사용합니다.

L264 case R_AARCH64_NONE:

AArch64의 NONE 재배치입니다. 수정할 주소나 명령이 없는 항목이므로 성공 상태만 설정합니다.

L265 ovf = 0;

수정 작업이 없는 NONE 항목은 오류가 없다고 기록합니다.

L266 break;

NONE 항목 처리를 끝내고 공통 결과 검사로 진행합니다.

L269 case R_AARCH64_ABS64:

S+A를 64비트 데이터에 그대로 기록하는 절대 재배치입니다. 기록 위치를 기준으로 한 상대 차이는 계산하지 않습니다.

L270 overflow_check = false;

64비트 전체를 담는 ABS64이므로 좁은 필드에 값이 잘리는 범위 검사를 하지 않습니다.

L271 ovf = reloc_data(RELOC_OP_ABS, loc, val, 64, me);

심벌 주소와 addend를 더한 절대값을 64비트 필드에 기록합니다. ABS는 기록 위치를 빼지 않는 계산 방식이며, 이 재배치에서는 64비트 값을 그대로 쓸 수 있어 별도의 좁은 폭 범위 검사를 끕니다.

L272 break;

64비트 절대값 쓰기를 마친 뒤 다른 재배치 종류의 처리를 건너뜁니다.

L273 case R_AARCH64_ABS32:

S+A를 32비트 unsigned 범위에 담아야 하는 절대 데이터 재배치입니다.

L274 ovf = reloc_data(RELOC_OP_ABS, loc, val, 32, me);

심벌 값과 addend의 합을 32비트 절대값으로 기록합니다. helper는 그 값이 32비트 unsigned 범위에 들어가는지도 확인하여, 주소가 잘려 의미가 달라질 경우 오류를 돌려줍니다.

L275 break;

ABS32 결과를 공통 검사로 넘기고 종류별 처리를 끝냅니다.

L276 case R_AARCH64_ABS16:

S+A를 16비트 unsigned 값으로 기록하는 절대 재배치입니다. 작은 필드가 표현할 수 없는 주소라면 오류가 됩니다.

L277 ovf = reloc_data(RELOC_OP_ABS, loc, val, 16, me);

심벌 값과 addend의 합을 16비트 절대값 필드에 기록합니다. 결과가 0부터 65535 범위를 벗어나면 -ERANGE가 되어 모듈 로더가 재배치 실패로 처리할 수 있습니다.

L278 break;

ABS16 기록과 범위 판정 결과를 가지고 switch에서 나갑니다.

L279 case R_AARCH64_PREL64:

S+A에서 기록 위치 P를 뺀 상대 거리를 64비트로 쓰는 재배치입니다.

L280 overflow_check = false;

PREL64은 상대 차이 전체를 64비트에 기록하므로 별도의 좁은 폭 범위 검사를 생략합니다.

L281 ovf = reloc_data(RELOC_OP_PREL, loc, val, 64, me);

심벌 값과 addend의 합에서 재배치 위치 자체의 주소를 뺀 차이를 64비트로 기록합니다. PREL은 절대 주소 대신 현재 위치를 기준으로 한 상대 거리를 저장하는 방식입니다.

L282 break;

64비트 상대값 쓰기를 마치고 공통 결과 처리로 진행합니다.

L283 case R_AARCH64_PREL32:

S+A-P의 상대 거리를 signed 32비트로 담는 재배치입니다. 양방향 거리를 나타내야 하므로 절대 재배치와 범위 기준이 다릅니다.

L284 ovf = reloc_data(RELOC_OP_PREL, loc, val, 32, me);

심벌과 addend가 가리키는 주소에서 재배치 위치를 뺀 상대 거리를 32비트 필드에 기록합니다. helper는 이 차이가 signed 32비트 범위에 들어가는지 확인하고 벗어나면 오류를 반환합니다.

L285 break;

PREL32 처리 결과를 남긴 채 종류별 분기를 마칩니다.

L286 case R_AARCH64_PREL16:

signed 16비트 상대 거리를 기록하는 재배치입니다. 목적지와 기록 위치가 이 범위 안에 있어야 합니다.

L287 ovf = reloc_data(RELOC_OP_PREL, loc, val, 16, me);

목적지 주소와 재배치 위치 사이의 차이를 16비트 필드에 기록합니다. PREL16은 signed 16비트 범위의 상대 거리만 표현하므로 그 범위를 넘으면 -ERANGE로 실패를 알립니다.

L288 break;

PREL16 처리 뒤 다른 MOVW 명령 재배치로 이어지지 않게 합니다.

L291 case R_AARCH64_MOVW_UABS_G0_NC:

절대값의 최하위 16비트를 MOVW 계열 명령에 넣되 상위 비트 존재를 오류로 보지 않는 G0_NC 종류입니다. NC는 범위 검사 생략을 뜻합니다.

L292 overflow_check = false;

이 G0_NC 항목에서는 helper가 감지한 상위 비트의 범위 초과를 허용합니다. 64비트 값을 여러 명령으로 나눠 구성할 때 필요한 동작입니다.

L293 fallthrough;

NC 정책만 설정한 뒤 다음 G0 분기의 실제 16비트 패치 코드를 함께 사용합니다. 의도적으로 다음 case로 이어진다는 표시입니다.

L294 case R_AARCH64_MOVW_UABS_G0:

절대값의 비트 15:0을 MOVW 즉시값에 넣는 G0 종류입니다. NC가 아닌 이 진입에서는 나머지 상위 값의 범위도 검사합니다.

L295 ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 0,

절대값을 오른쪽으로 0비트 이동한 부분을 16비트 즉시값으로 패치합니다. 즉 최하위 16비트를 선택합니다.

L296 AARCH64_INSN_IMM_MOVKZ, me);

MOVK/MOVZ 계열의 명령 형태를 유지하는 모드를 넘깁니다. signed 재배치처럼 부호에 따라 MOVN으로 opcode를 바꾸는 모드를 쓰지 않습니다.

L297 break;

G0의 명령 패치 결과를 남기고 공통 범위 검사로 이동합니다.

L298 case R_AARCH64_MOVW_UABS_G1_NC:

절대값의 비트 31:16을 넣는 G1 중 범위 검사를 생략하는 종류입니다. 더 높은 조각은 다른 MOVW 명령이 만들 수 있습니다.

L299 overflow_check = false;

G1_NC에서는 선택한 조각 위에 값이 남아 있어도 허용하도록 설정합니다.

L300 fallthrough;

NC 설정 후 일반 G1과 동일한 즉시값 삽입 코드를 실행합니다.

L301 case R_AARCH64_MOVW_UABS_G1:

절대값을 16비트 이동한 뒤 그 하위 16비트를 명령에 넣는 G1 종류입니다.

L302 ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 16,

S+A를 16비트 오른쪽으로 옮겨 비트 31:16을 이번 명령의 즉시값으로 선택합니다.

L303 AARCH64_INSN_IMM_MOVKZ, me);

G1에서도 원래 MOVK/MOVZ opcode 형태를 유지하며 즉시값 필드를 고치도록 지정합니다.

L304 break;

G1 조각의 패치를 마치고 공통 결과 검사로 진행합니다.

L305 case R_AARCH64_MOVW_UABS_G2_NC:

절대값의 비트 47:32를 쓰는 G2 중 상위 비트 범위 검사를 생략하는 재배치입니다.

L306 overflow_check = false;

G2_NC는 48비트보다 높은 부분이 다른 명령에 들어갈 수 있으므로 범위 검사를 끕니다.

L307 fallthrough;

검사 정책을 정한 뒤 아래 G2의 실제 패치를 공유합니다.

L308 case R_AARCH64_MOVW_UABS_G2:

절대값을 32비트 이동해 비트 47:32를 MOVW 즉시값으로 쓰는 종류입니다.

L309 ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 32,

S+A에서 32비트 위의 16비트 조각을 추출하여 명령에 넣도록 helper를 호출합니다.

L310 AARCH64_INSN_IMM_MOVKZ, me);

G2 조각도 MOVK/MOVZ 형식을 유지하며 기록하고 모듈 상태에 맞는 쓰기 방식을 사용하도록 모듈 자료를 넘깁니다.

L311 break;

G2 패치가 끝났으므로 다음 G3 종류로 이어지지 않고 공통 검사로 이동합니다.

L312 case R_AARCH64_MOVW_UABS_G3:

64비트 절대값의 최상위 조각인 비트 63:48을 MOVW 즉시값에 넣는 종류입니다.

L314 overflow_check = false;

최상위 16비트를 선택하면 그 위에 남을 값이 없으므로 범위 검사를 끕니다.

L315 ovf = reloc_insn_movw(RELOC_OP_ABS, loc, val, 48,

64비트 절대값을 48비트 이동하여 마지막 16비트 조각을 명령의 즉시값으로 기록합니다.

x86-64 · Linux 6.18.37

R_X86_64_64, PC32/PLT32와 alternatives

absolute 64-bit와 signed 32-bit PC-relative relocation이 중심이다. module을 kernel text와 가까운 범위에 배치해 rel32 call/jump의 ±2GiB 제한을 맞춘다. 이 x86-64 relocation 함수에는 거리 초과 시 PLT를 만드는 fallback이 없다.

원본 코드: arch/x86/kernel/module.c:208-294

208	if (!early) {
209		smp_text_poke_sync_each_cpu();
210		mutex_unlock(&text_mutex);
211	}
212 
213	return ret;
214}
215 
216int apply_relocate_add(Elf64_Shdr *sechdrs,
217		   const char *strtab,
218		   unsigned int symindex,
219		   unsigned int relsec,
220		   struct module *me)
221{
222	return write_relocate_add(sechdrs, strtab, symindex, relsec, me, true);
223}
224 
225#ifdef CONFIG_LIVEPATCH
226void clear_relocate_add(Elf64_Shdr *sechdrs,
227			const char *strtab,
228			unsigned int symindex,
229			unsigned int relsec,
230			struct module *me)
231{
232	write_relocate_add(sechdrs, strtab, symindex, relsec, me, false);
233}
234#endif
235 
236#endif
237 
238int module_finalize(const Elf_Ehdr *hdr,
239		    const Elf_Shdr *sechdrs,
240		    struct module *me)
241{
242	const Elf_Shdr *s, *alt = NULL, *locks = NULL,
243		*orc = NULL, *orc_ip = NULL,
244		*retpolines = NULL, *returns = NULL, *ibt_endbr = NULL,
245		*calls = NULL, *cfi = NULL;
246	char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;
247 
248	for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {
249		if (!strcmp(".altinstructions", secstrings + s->sh_name))
250			alt = s;
251		if (!strcmp(".smp_locks", secstrings + s->sh_name))
252			locks = s;
253		if (!strcmp(".orc_unwind", secstrings + s->sh_name))
254			orc = s;
255		if (!strcmp(".orc_unwind_ip", secstrings + s->sh_name))
256			orc_ip = s;
257		if (!strcmp(".retpoline_sites", secstrings + s->sh_name))
258			retpolines = s;
259		if (!strcmp(".return_sites", secstrings + s->sh_name))
260			returns = s;
261		if (!strcmp(".call_sites", secstrings + s->sh_name))
262			calls = s;
263		if (!strcmp(".cfi_sites", secstrings + s->sh_name))
264			cfi = s;
265		if (!strcmp(".ibt_endbr_seal", secstrings + s->sh_name))
266			ibt_endbr = s;
267	}
268 
269	its_init_mod(me);
270 
271	if (retpolines || cfi) {
272		void *rseg = NULL, *cseg = NULL;
273		unsigned int rsize = 0, csize = 0;
274 
275		if (retpolines) {
276			rseg = (void *)retpolines->sh_addr;
277			rsize = retpolines->sh_size;
278		}
279 
280		if (cfi) {
281			cseg = (void *)cfi->sh_addr;
282			csize = cfi->sh_size;
283		}
284 
285		apply_fineibt(rseg, rseg + rsize, cseg, cseg + csize);
286	}
287	if (retpolines) {
288		void *rseg = (void *)retpolines->sh_addr;
289		apply_retpolines(rseg, rseg + retpolines->sh_size);
290	}
291 
292	its_fini_mod(me);
293 
294	if (returns) {

주요 구문 해설

원본 87줄은 그대로 표시하며, 검토한 59개 구문에 설명을 붙였습니다.

L208 if (!early) {

모듈이 아직 형성 중인 초기 쓰기가 아닌지 확인합니다. 이미 형성된 코드는 CPU 간 명령 동기화와 text_mutex 해제가 필요합니다.

L209 smp_text_poke_sync_each_cpu();

이미 사용 가능한 모듈의 코드를 수정한 경우, 모든 CPU가 수정된 명령을 보도록 text poke 동기화를 수행합니다. 단순히 데이터 쓰기가 끝난 것과 다른 CPU의 명령 실행이 새 코드로 맞춰진 것은 별개이므로 이 단계가 필요합니다.

L210 mutex_unlock(&text_mutex);

실행 중인 코드의 재배치 쓰기와 CPU 간 명령 동기화를 마쳤으므로 text_mutex를 해제합니다. 같은 코드 영역을 바꾸는 다른 작업이 이제 진행할 수 있습니다.

L213 return ret;

모듈 재배치 적용 또는 해제의 결과를 호출자에게 돌려줍니다. 정상 완료는 0이고 지원하지 않는 형식·값 불일치·범위 초과는 -ENOEXEC이며, 실행 중 코드에 text_poke를 쓴 경우에는 앞에서 CPU 동기화와 잠금 해제를 마친 상태입니다.

L216int apply_relocate_add(Elf64_Shdr *sechdrs,

x86 모듈의 RELA 섹션을 적용하는 공개 함수입니다. 아래 공통 쓰기 함수에 apply=true를 넘겨 실제 재배치 값을 기록합니다.

L217 const char *strtab,

심벌 이름 문자열 표를 전달받습니다. 심벌을 식별하는 공통 재배치 함수 인터페이스의 인자입니다.

L218 unsigned int symindex,

참조 심벌들이 저장된 심벌 표 섹션 번호입니다.

L219 unsigned int relsec,

이번에 적용할 RELA 섹션 번호입니다. 이 섹션의 항목들이 실제 코드·데이터 수정 위치를 지정합니다.

L220 struct module *me)

수정 대상 모듈을 받습니다. 초기 적재 중인지 이미 형성된 모듈인지에 따라 memcpy 또는 text_poke와 잠금 사용이 달라집니다.

L222 return write_relocate_add(sechdrs, strtab, symindex, relsec, me, true);

공통 helper에 적용 모드 true를 넘기고 그 성공·오류 코드를 그대로 반환합니다. 같은 helper의 false 모드는 livepatch 재배치 해제에 사용됩니다.

L225#ifdef CONFIG_LIVEPATCH

LIVEPATCH는 실행 중인 커널의 함수를 교체하는 기능입니다. 지원 빌드에서는 패치 모듈의 RELA 재배치를 지울 clear_relocate_add도 제공하여 재배치 적용·해제 과정을 관리합니다. 설정이 없으면 이 해제용 함수는 빌드하지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L226void clear_relocate_add(Elf64_Shdr *sechdrs,

livepatch가 적용한 RELA 값을 지우는 함수입니다. 적용할 때와 같은 섹션 정보를 받아 예상값을 확인한 뒤 해당 필드를 0으로 돌립니다.

L227 const char *strtab,

해제 대상 재배치에 연결된 심벌 이름 표를 받습니다. 적용 함수와 같은 인자 구조를 유지합니다.

L228 unsigned int symindex,

해제할 재배치의 원래 값을 계산하는 데 사용할 심벌 표의 섹션 번호입니다.

L229 unsigned int relsec,

값을 제거할 RELA 기록들의 섹션 번호입니다. 해당 기록으로 수정 위치와 원래 적용값을 다시 찾습니다.

L230 struct module *me)

재배치 값을 지울 모듈 자료입니다. 코드 쓰기 안전성과 모듈 상태 판단에도 사용됩니다.

L232 write_relocate_add(sechdrs, strtab, symindex, relsec, me, false);

apply 인자를 false로 주어 앞서 적용한 재배치 값을 지우는 경로를 호출합니다. helper는 현재 값이 예상한 재배치 결과인지 검사하고 해당 필드를 0으로 돌려, livepatch 재배치의 해제를 처리합니다.

L234#endif

225행에서 시작한 선택 구간을 마칩니다. LIVEPATCH는 실행 중인 커널의 함수를 교체하는 기능입니다. 지원 빌드에서는 패치 모듈의 RELA 재배치를 지울 clear_relocate_add도 제공하여 재배치 적용·해제 과정을 관리합니다. 설정이 없으면 이 해제용 함수는 빌드하지 않습니다.

L236#endif

40행에서 시작한 선택 구간을 마칩니다. 32비트 모듈의 ELF32 재배치와 64비트 모듈의 ELF64 RELA 처리를 나누는 조건입니다. 주소 폭과 ELF 재배치 형식이 달라 같은 구현을 그대로 사용할 수 없습니다.

L238int module_finalize(const Elf_Ehdr *hdr,

재배치 이후 x86 모듈의 CPU별 명령 패치와 보안·스택 추적 자료를 마무리하는 함수입니다. ELF 헤더로 필요한 부가 섹션을 찾아 처리합니다.

L239 const Elf_Shdr *sechdrs,

각 섹션의 이름·적재 주소·크기를 가진 섹션 헤더 배열을 받습니다. 문자열 표와 대조해 패치 자료를 찾습니다.

L240 struct module *me)

마무리할 모듈 자료입니다. thunk 생성과 명령 패치, ORC 등록 등의 작업이 이 모듈에 적용됩니다.

L242 const Elf_Shdr *s, *alt = NULL, *locks = NULL,

현재 순회하는 섹션과 alternative 명령·SMP 잠금 위치 섹션의 포인터를 준비합니다. NULL은 해당 부가 섹션을 아직 찾지 못했다는 뜻입니다.

L246 char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;

ELF 헤더의 섹션 이름 문자열 표 인덱스를 사용해 이름 표 시작을 구합니다. 각 sh_name은 이 표 안의 바이트 오프셋입니다.

L248 for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {

ELF의 모든 섹션 헤더를 순회하며 x86 마무리 작업에 필요한 이름들을 찾습니다.

L249 if (!strcmp(".altinstructions", secstrings + s->sh_name))

현재 섹션 이름이 .altinstructions인지 비교합니다. CPU 기능에 따라 교체할 명령의 메타데이터를 담는 섹션입니다.

L250 alt = s;

찾은 alternative 명령 섹션을 기억해 뒤에서 실제 CPU 기능에 맞는 명령 패치를 적용합니다.

L251 if (!strcmp(".smp_locks", secstrings + s->sh_name))

SMP LOCK 접두어를 조정할 위치 목록인 .smp_locks를 찾습니다. CPU 수와 SMP 동작 방식에 맞춘 명령 조정을 위한 자료입니다.

L252 locks = s;

찾은 SMP 잠금 위치 섹션을 마무리 단계가 사용할 수 있도록 저장합니다.

L253 if (!strcmp(".orc_unwind", secstrings + s->sh_name))

ORC 스택 추적기가 사용할 복원 규칙 섹션을 찾습니다. 프레임 포인터 없이도 호출 스택을 복원하기 위한 메타데이터입니다.

L254 orc = s;

ORC 복원 규칙이 든 섹션을 기억합니다. 대응하는 명령 주소 목록과 함께 등록할 때 사용합니다.

L255 if (!strcmp(".orc_unwind_ip", secstrings + s->sh_name))

ORC 규칙에 대응하는 명령 주소 목록인 .orc_unwind_ip를 찾습니다. 각 코드 위치에 어떤 복원 규칙을 적용할지 연결합니다.

L256 orc_ip = s;

ORC 명령 주소 섹션을 보관하여 뒤에서 ORC 규칙 섹션과 함께 처리합니다.

L257 if (!strcmp(".retpoline_sites", secstrings + s->sh_name))

간접 분기의 retpoline 패치 위치 목록을 찾습니다. 실제 CPU의 분기 예측 완화 정책에 맞춰 바꿀 지점들입니다.

L258 retpolines = s;

retpoline 위치 목록을 저장합니다. FineIBT 준비와 이후 간접 분기 패치가 이 정보를 사용합니다.

L259 if (!strcmp(".return_sites", secstrings + s->sh_name))

함수 반환 명령의 패치 위치 목록인 .return_sites를 찾습니다. 반환 분기에 적용할 CPU별 완화 처리를 위한 자료입니다.

L260 returns = s;

반환 명령 위치 섹션을 보관해 apply_returns 단계에 넘길 준비를 합니다.

L261 if (!strcmp(".call_sites", secstrings + s->sh_name))

모듈 안의 호출 지점 목록인 .call_sites를 찾습니다. call thunk를 적용할 호출들을 식별합니다.

L262 calls = s;

찾은 호출 지점 목록을 저장하여 뒤의 call thunk 패치가 사용할 수 있게 합니다.

L263 if (!strcmp(".cfi_sites", secstrings + s->sh_name))

간접 호출의 유형 검사 대상 목록인 .cfi_sites를 찾습니다. CFI 정책에 맞는 함수 진입 코드 패치에 사용합니다.

L264 cfi = s;

CFI 대상 목록을 기억해 간접 호출 위치와 함께 FineIBT 처리에 넘깁니다.

L265 if (!strcmp(".ibt_endbr_seal", secstrings + s->sh_name))

간접 분기 진입점으로 필요하지 않은 ENDBR을 봉인할 목록을 찾습니다. IBT가 허용하는 진입점을 불필요하게 넓히지 않기 위한 자료입니다.

L266 ibt_endbr = s;

ENDBR 봉인 대상 목록을 저장합니다. 이후 모듈 IBT 정리 단계에서 해당 명령을 처리합니다.

L269 its_init_mod(me);

이 모듈에 x86 ITS 완화용 간접 분기 thunk를 만들 준비를 합니다. 필요한 CPU에서 text_mutex를 잡고 대상 모듈과 thunk 페이지 상태를 등록하며, Arm 인터럽트 컨트롤러의 ITS와는 다른 기능입니다.

L271 if (retpolines || cfi) {

간접 호출 위치나 CFI 대상 자료 중 하나라도 있으면 FineIBT 준비를 수행합니다. 둘 다 없는 모듈은 관련 패치를 건너뜁니다.

L272 void *rseg = NULL, *cseg = NULL;

간접 호출 위치 목록과 CFI 목록의 시작 주소를 NULL로 준비합니다. 한쪽 섹션이 없을 때도 공통 함수에 빈 범위를 전달할 수 있습니다.

L273 unsigned int rsize = 0, csize = 0;

두 목록의 크기를 0으로 시작합니다. 해당 섹션을 찾은 경우에만 실제 바이트 수를 채워 없는 목록의 범위를 비워 둡니다.

L275 if (retpolines) {

retpoline 위치 섹션이 실제로 있을 때 그 주소와 길이를 읽습니다. 없는 모듈에서는 NULL·0을 유지합니다.

L276 rseg = (void *)retpolines->sh_addr;

적재된 retpoline 위치 목록의 시작 주소를 FineIBT용 입력에 넣습니다.

L277 rsize = retpolines->sh_size;

retpoline 위치 목록의 바이트 수를 가져와 끝 주소를 계산할 수 있게 합니다.

L280 if (cfi) {

CFI 대상 목록이 존재하면 그 범위를 설정합니다. 없으면 앞서 준비한 빈 범위를 사용합니다.

L281 cseg = (void *)cfi->sh_addr;

적재된 CFI 대상 목록의 시작 주소를 보관합니다.

L282 csize = cfi->sh_size;

CFI 목록의 크기를 저장해 apply_fineibt에 전달할 끝 주소를 계산합니다.

L285 apply_fineibt(rseg, rseg + rsize, cseg, cseg + csize);

모듈의 간접 호출 지점 목록과 CFI 대상 목록을 함께 넘겨 현재 커널의 CFI 정책에 맞게 코드를 조정합니다. FineIBT를 사용하는 경우 함수 유형 검사와 IBT를 결합한 간접 호출 보호가 모듈에도 적용되도록 합니다.

L287 if (retpolines) {

retpoline 위치 목록이 있는 모듈에만 실제 간접 분기 패치를 수행합니다. CFI 준비와 별개로 CPU의 retpoline 정책을 적용하는 단계입니다.

L288 void *rseg = (void *)retpolines->sh_addr;

간접 분기 패치가 시작할 retpoline 위치 목록의 주소를 가져옵니다.

L289 apply_retpolines(rseg, rseg + retpolines->sh_size);

모듈의 retpoline 위치 목록을 따라 간접 분기 명령을 현재 CPU와 완화 정책에 맞게 패치합니다. 컴파일할 때 만든 간접 분기 보호 코드가 실제 시스템에서 선택한 실행 방식과 일치하도록 하는 단계입니다.

L292 its_fini_mod(me);

모듈의 ITS thunk 생성을 마무리하여 임시 모듈·페이지 상태를 비우고 잠금을 풉니다. 엄격한 모듈 메모리 권한 설정에서는 생성한 thunk 페이지도 실행 가능하고 쓰기 불가능한 상태로 정리합니다.

L294 if (returns) {

반환 명령 위치 목록이 있으면 반환 분기 완화 패치를 적용합니다. 해당 섹션이 없는 모듈은 이 처리를 생략합니다.

RISC-V · Linux 6.18.37

HI20/LO12 pair, CALL_PLT, JAL/BRANCH와 relaxation

AUIPC+JALR 또는 LUI+ADDI처럼 여러 instruction이 하나의 symbol address를 구성하므로 paired relocation을 일관되게 적용한다. 먼 target에는 toolchain이 AUIPC+JALR의 CALL 또는 CALL_PLT relocation을 내야 한다. JAL relocation 자체는 PLT로 자동 변환되지 않는다.

원본 코드: arch/riscv/kernel/module.c:759-855

759	if (!*relocation_hashtable)
760		return 0;
761 
762	__hash_init(*relocation_hashtable, hashtable_size);
763 
764	return hashtable_bits;
765}
766 
767int apply_relocate_add(Elf_Shdr *sechdrs, const char *strtab,
768		       unsigned int symindex, unsigned int relsec,
769		       struct module *me)
770{
771	Elf_Rela *rel = (void *) sechdrs[relsec].sh_addr;
772	int (*handler)(struct module *me, void *location, Elf_Addr v);
773	Elf_Sym *sym;
774	void *location;
775	unsigned int i, type;
776	unsigned int j_idx = 0;
777	Elf_Addr v;
778	int res;
779	unsigned int num_relocations = sechdrs[relsec].sh_size / sizeof(*rel);
780	struct hlist_head *relocation_hashtable;
781	unsigned int hashtable_bits;
782	LIST_HEAD(used_buckets_list);
783 
784	hashtable_bits = initialize_relocation_hashtable(num_relocations,
785							 &relocation_hashtable);
786 
787	if (!relocation_hashtable)
788		return -ENOMEM;
789 
790	pr_debug("Applying relocate section %u to %u\n", relsec,
791	       sechdrs[relsec].sh_info);
792 
793	for (i = 0; i < num_relocations; i++) {
794		/* This is where to make the change */
795		location = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr
796			+ rel[i].r_offset;
797		/* This is the symbol it is referring to */
798		sym = (Elf_Sym *)sechdrs[symindex].sh_addr
799			+ ELF_RISCV_R_SYM(rel[i].r_info);
800		if (IS_ERR_VALUE(sym->st_value)) {
801			/* Ignore unresolved weak symbol */
802			if (ELF_ST_BIND(sym->st_info) == STB_WEAK)
803				continue;
804			pr_warn("%s: Unknown symbol %s\n",
805				me->name, strtab + sym->st_name);
806			return -ENOENT;
807		}
808 
809		type = ELF_RISCV_R_TYPE(rel[i].r_info);
810 
811		if (type < ARRAY_SIZE(reloc_handlers))
812			handler = reloc_handlers[type].reloc_handler;
813		else
814			handler = NULL;
815 
816		if (!handler) {
817			pr_err("%s: Unknown relocation type %u\n",
818			       me->name, type);
819			return -EINVAL;
820		}
821 
822		v = sym->st_value + rel[i].r_addend;
823 
824		if (type == R_RISCV_PCREL_LO12_I || type == R_RISCV_PCREL_LO12_S) {
825			unsigned int j = j_idx;
826			bool found = false;
827 
828			do {
829				unsigned long hi20_loc =
830					sechdrs[sechdrs[relsec].sh_info].sh_addr
831					+ rel[j].r_offset;
832				u32 hi20_type = ELF_RISCV_R_TYPE(rel[j].r_info);
833 
834				/* Find the corresponding HI20 relocation entry */
835				if (hi20_loc == sym->st_value
836				    && (hi20_type == R_RISCV_PCREL_HI20
837					|| hi20_type == R_RISCV_GOT_HI20)) {
838					s32 hi20, lo12;
839					Elf_Sym *hi20_sym =
840						(Elf_Sym *)sechdrs[symindex].sh_addr
841						+ ELF_RISCV_R_SYM(rel[j].r_info);
842					unsigned long hi20_sym_val =
843						hi20_sym->st_value
844						+ rel[j].r_addend;
845 
846					/* Calculate lo12 */
847					size_t offset = hi20_sym_val - hi20_loc;
848					if (IS_ENABLED(CONFIG_MODULE_SECTIONS)
849					    && hi20_type == R_RISCV_GOT_HI20) {
850						offset = module_emit_got_entry(
851							 me, hi20_sym_val);
852						offset = offset - hi20_loc;
853					}
854					hi20 = (offset + 0x800) & 0xfffff000;
855					lo12 = offset - hi20;

주요 구문 해설

원본 97줄은 그대로 표시하며, 검토한 72개 구문에 설명을 붙였습니다.

L759 if (!*relocation_hashtable)

누적 재배치를 위한 해시 버킷 배열 할당이 실패했는지 확인합니다. 실패한 NULL 배열을 초기화하거나 사용하지 않도록 합니다.

L760 return 0;

할당 실패 시 비트 수는 0으로 돌려줍니다. 크기 1인 정상 해시도 비트 수가 0일 수 있으므로 호출자는 결과 포인터의 NULL 여부로 실패를 판단합니다.

L762 __hash_init(*relocation_hashtable, hashtable_size);

재배치 기록을 모을 해시 표의 모든 버킷을 빈 목록으로 초기화합니다. 뒤에서 같은 위치에 누적되는 재배치들을 추적할 수 있도록, 새로 할당한 표를 곧바로 검색·삽입 가능한 상태로 만듭니다.

L764 return hashtable_bits;

해시 버킷 수가 2의 몇 제곱인지를 반환합니다. 이후 주소를 버킷 인덱스로 변환할 때 사용할 비트 수입니다.

L767int apply_relocate_add(Elf_Shdr *sechdrs, const char *strtab,

RISC-V 모듈의 RELA 재배치를 적용하는 함수입니다. 섹션 정보와 심벌 이름 표를 받아 명령·데이터 주소를 실제 적재 위치에 맞춥니다.

L768 unsigned int symindex, unsigned int relsec,

심벌 표 섹션 번호와 처리할 재배치 섹션 번호를 받습니다. 두 번호로 심벌 값과 수정 위치를 서로 연결합니다.

L769 struct module *me)

대상 모듈 자료를 받습니다. 오류에 모듈 이름을 표시하고 GOT·PLT 등 모듈 전용 보조 공간을 만들 때 사용합니다.

L771 Elf_Rela *rel = (void *) sechdrs[relsec].sh_addr;

재배치 섹션의 적재 주소를 Elf_Rela 배열로 해석합니다. 각 항목의 별도 addend와 종류·심벌 정보를 순서대로 읽습니다.

L772 int (*handler)(struct module *me, void *location, Elf_Addr v);

모듈 정보·수정할 주소·재배치 값을 받아 성공 또는 오류를 int로 반환하는 함수 포인터를 선언합니다. 재배치 종류에 따라 처리 함수를 나중에 선택할 변수이며, 이 줄에서 함수를 호출하지 않습니다.

L773 Elf_Sym *sym;

현재 항목이 참조하는 ELF 심벌의 포인터입니다. 주소를 구할 수 있는지와 weak 심벌인지 판단할 때도 사용합니다.

L774 void *location;

이번 항목이 수정할 메모리 주소를 저장합니다. 대상 섹션의 적재 주소와 r_offset을 더해 계산합니다.

L775 unsigned int i, type;

i는 현재 재배치 항목 번호이고 type은 그 항목의 RISC-V 재배치 종류입니다. 종류에 따라 적용 함수를 고릅니다.

L776 unsigned int j_idx = 0;

직전에 HI20 짝을 찾던 검색 위치를 0으로 시작합니다. 이후 LO12 항목마다 검색을 처음부터 되풀이하는 부담을 줄이는 시작점입니다.

L777 Elf_Addr v;

심벌 값과 addend로 계산한 주소 또는 즉시값을 담습니다. PCREL_LO12는 짝인 HI20을 찾은 뒤 하위 조각 값으로 다시 계산합니다.

L778 int res;

개별 재배치 처리 또는 누적 기록 추가의 결과를 담는 오류 코드입니다.

L779 unsigned int num_relocations = sechdrs[relsec].sh_size / sizeof(*rel);

재배치 섹션의 전체 바이트 수를 Elf_Rela 한 항목의 크기로 나누어 항목 수를 구합니다. sizeof는 실제 항목을 실행하거나 읽는 함수가 아니라, 반복문이 처리할 기록 수를 계산하기 위한 C 연산자입니다.

L780 struct hlist_head *relocation_hashtable;

같은 위치에 누적 적용할 재배치들을 모으는 해시 버킷 배열입니다. 즉시 한 번 쓰는 재배치와 별도로 관리합니다.

L781 unsigned int hashtable_bits;

해시 테이블의 버킷 수를 나타내는 로그2 값입니다. 재배치 위치를 버킷으로 배치할 때 사용합니다.

L782 LIST_HEAD(used_buckets_list);

사용한 해시 버킷들을 연결해 둘 빈 이중 연결 목록의 머리를 선언하고 초기화합니다. 이후 정리할 때 전체 해시 표 대신 실제 사용한 버킷들을 따라갈 수 있게 합니다.

L784 hashtable_bits = initialize_relocation_hashtable(num_relocations,

재배치 개수에 맞는 해시 버킷 배열을 만들고 사용할 인덱스 비트 수를 받습니다. 누적 재배치를 모을 공간을 실제 처리를 시작하기 전에 준비합니다.

L785 &relocation_hashtable);

할당한 배열 주소를 이 지역 포인터에 써 달라고 넘깁니다. 반환 비트 수 0만으로는 실패를 판별할 수 없어 포인터도 함께 받습니다.

L787 if (!relocation_hashtable)

해시 배열 포인터가 NULL이면 메모리 확보에 실패한 것입니다. 정상 크기 1에서도 비트 수는 0일 수 있어 포인터로 검사합니다.

L788 return -ENOMEM;

누적 재배치에 필요한 메모리를 확보하지 못했으므로 -ENOMEM으로 모듈 적재 실패를 알립니다.

L790 pr_debug("Applying relocate section %u to %u\n", relsec,

어느 재배치 섹션을 적용하는지 디버그 로그를 남깁니다. 실행 동작을 바꾸는 코드가 아니라 적재 과정을 추적하는 정보입니다.

L791 sechdrs[relsec].sh_info);

로그에 실제 수정 대상 섹션 번호도 넣습니다. sh_info가 재배치 기록과 그 기록이 수정할 섹션을 연결합니다.

L793 for (i = 0; i < num_relocations; i++) {

RELA 섹션의 모든 항목을 순회합니다. 각 항목에서 수정 위치·심벌·종류를 읽고 알맞은 계산을 수행합니다.

L795 location = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr

재배치 섹션의 sh_info로 실제 대상 섹션을 찾아 그 적재 주소를 얻습니다. 수정 위치 계산의 기준 주소입니다.

L796 + rel[i].r_offset;

해당 대상 섹션 시작에 r_offset을 더해 이번 재배치가 고칠 명령이나 데이터 위치를 정합니다.

L798 sym = (Elf_Sym *)sechdrs[symindex].sh_addr

심벌 표의 적재 주소를 Elf_Sym 배열로 변환합니다. 다음 줄의 r_info 심벌 인덱스가 그중 사용할 항목을 선택합니다.

L799 + ELF_RISCV_R_SYM(rel[i].r_info);

현재 재배치의 r_info에서 RISC-V ELF 심벌 인덱스를 꺼내 심벌 표의 항목을 선택합니다. 그 항목의 st_value가 addend와 함께 주소 계산의 입력이 됩니다.

L800 if (IS_ERR_VALUE(sym->st_value)) {

심벌 값이 정상 주소가 아니라 해석 실패를 나타내는 오류값인지 확인합니다. weak 심벌이면 건너뛸 수 있지만 일반 심벌은 적재 오류가 됩니다.

L802 if (ELF_ST_BIND(sym->st_info) == STB_WEAK)

주소를 찾지 못한 심벌의 결합 속성이 WEAK인지 확인합니다. 선택적 참조를 허용하는 weak 심벌만 미해결 상태를 무시합니다.

L803 continue;

미해결 weak 심벌의 항목은 수정하지 않고 다음 재배치로 넘어갑니다.

L804 pr_warn("%s: Unknown symbol %s\n",

필수 심벌을 해석하지 못했으므로 모듈 이름과 심벌 이름을 경고합니다.

L805 me->name, strtab + sym->st_name);

심벌 이름은 문자열 표 시작에 st_name 오프셋을 더해 얻습니다. 실패한 참조를 사람이 식별할 수 있도록 로그에 전달합니다.

L806 return -ENOENT;

필수 심벌을 찾지 못한 오류를 -ENOENT로 반환합니다. 해석되지 않은 값을 주소처럼 기록하지 않습니다.

L809 type = ELF_RISCV_R_TYPE(rel[i].r_info);

r_info에서 재배치 종류 번호를 꺼냅니다. 이 값으로 처리 함수와 HI20·LO12 같은 특별 처리가 필요한 종류를 구분하며, 심벌 인덱스와 같은 필드에 포장되어 있어 각각 따로 추출합니다.

L811 if (type < ARRAY_SIZE(reloc_handlers))

재배치 종류가 처리 함수 배열의 범위 안인지 확인합니다. 범위를 먼저 검사하여 잘못된 ELF 입력으로 배열 밖 함수를 읽지 않게 합니다.

L812 handler = reloc_handlers[type].reloc_handler;

이번 재배치 종류에 등록된 처리 함수를 선택합니다. 배열 범위 안이어도 지원하지 않는 종류의 슬롯은 NULL일 수 있습니다.

L813 else

종류 번호가 처리 배열 범위를 벗어나면 지원하지 않는 재배치로 취급합니다.

L814 handler = NULL;

유효한 처리 함수가 없다는 표시로 NULL을 넣습니다. 아래 공통 검사에서 적재를 중단하게 됩니다.

L816 if (!handler) {

종류가 범위 밖이거나 해당 슬롯에 구현이 없는지 확인합니다. 어느 쪽이든 임의의 함수를 실행하지 않고 재배치 오류를 보고합니다.

L817 pr_err("%s: Unknown relocation type %u\n",

지원하지 않는 재배치 종류 번호를 모듈 이름과 함께 오류 로그에 남깁니다.

L818 me->name, type);

오류 메시지에 대상 모듈 이름과 실제 종류 번호를 넘깁니다. 잘못된 ELF 파일이나 지원 범위를 진단할 단서입니다.

L819 return -EINVAL;

지원하지 않는 type, 잘못된 pair 또는 overflow relocation을 거부한다.

L822 v = sym->st_value + rel[i].r_addend;

심벌의 실제 값 S에 RELA의 addend A를 더해 기본 재배치 값 S+A를 만듭니다.

L824 if (type == R_RISCV_PCREL_LO12_I || type == R_RISCV_PCREL_LO12_S) {

하위 12비트를 I형 또는 S형 명령에 넣는 PC 상대 재배치인지 확인합니다. 이 경우 LO12 심벌 자체가 목적지가 아니라 짝인 HI20 명령 위치를 가리키므로 추가 검색이 필요합니다.

L825 unsigned int j = j_idx;

이전 HI20 검색이 끝난 위치부터 이번 짝 검색을 시작합니다. 연속된 재배치 기록에서 불필요한 재탐색을 줄입니다.

L826 bool found = false;

아직 짝인 HI20 항목을 찾지 못했다고 초기화합니다. 검색을 다 마쳐도 false면 LO12를 계산할 수 없어 적재 오류가 됩니다.

L828 do {

후보 재배치를 순환하며 일치하는 HI20을 찾습니다. 시작 후보도 먼저 검사한 뒤 다음 위치로 이동하는 검색입니다.

L829 unsigned long hi20_loc =

현재 후보의 HI20 명령이 놓일 주소를 계산할 변수를 정의합니다. LO12 심벌이 가리키는 위치와 비교할 값입니다.

L830 sechdrs[sechdrs[relsec].sh_info].sh_addr

HI20 후보가 적용되는 대상 섹션의 실제 시작 주소를 가져옵니다.

L831 + rel[j].r_offset;

후보 재배치의 r_offset을 더해 HI20 후보 명령의 실제 주소를 완성합니다.

L832 u32 hi20_type = ELF_RISCV_R_TYPE(rel[j].r_info);

LO12 재배치와 짝이 될 수 있는 후보 기록의 재배치 종류를 읽습니다. 후보가 PCREL_HI20 또는 GOT_HI20인지 확인하여, 하위 비트 계산에 쓸 상위 주소 계산 명령을 찾습니다.

L835 if (hi20_loc == sym->st_value

후보 명령 위치가 LO12 심벌이 가리키는 HI20 위치와 같은지 확인합니다. 목적 심벌 주소가 아니라 짝이 되는 명령 주소를 비교합니다.

L836 && (hi20_type == R_RISCV_PCREL_HI20

주소가 일치하는 후보가 PCREL_HI20인지 확인합니다. AUIPC 등의 상위 주소 계산과 LO12를 묶기 위한 종류 검사입니다.

L837 || hi20_type == R_RISCV_GOT_HI20)) {

GOT_HI20도 LO12의 짝으로 허용합니다. 이 경우 실제 목적지 대신 GOT 항목 주소까지의 상대 거리를 계산해야 합니다.

L838 s32 hi20, lo12;

상위 20비트에 해당하는 정렬 값과 부호 있는 하위 12비트 값을 담습니다. 둘을 합쳐 원래 PC 상대 거리를 재구성합니다.

L839 Elf_Sym *hi20_sym =

짝인 HI20이 참조하는 실제 목적 심벌을 가리킬 포인터입니다. LO12 심벌과 역할이 다르므로 별도로 구합니다.

L840 (Elf_Sym *)sechdrs[symindex].sh_addr

HI20 목적 심벌을 찾기 위해 ELF 심벌 표의 시작 주소를 사용합니다. 다음 줄이 HI20 항목의 심벌 인덱스를 더합니다.

L841 + ELF_RISCV_R_SYM(rel[j].r_info);

찾아낸 HI20 재배치가 참조하는 실제 목적 심벌을 선택합니다. LO12 쪽 심벌은 HI20 명령 위치를 가리킬 수 있으므로, 최종 목적지의 주소는 이 짝의 심벌에서 가져와야 합니다.

L842 unsigned long hi20_sym_val =

HI20 재배치의 심벌과 addend를 합친 실제 목적값을 담습니다.

L843 hi20_sym->st_value

짝인 HI20 항목이 참조하는 심벌의 값을 읽습니다. LO12가 참조한 HI20 명령 주소와 혼동하지 않아야 하는 목적값입니다.

L844 + rel[j].r_addend;

그 HI20 항목의 addend를 더해 최종 S+A를 만듭니다. 서로 다른 RELA 항목의 addend를 섞지 않습니다.

L847 size_t offset = hi20_sym_val - hi20_loc;

HI20의 목적값에서 HI20 명령 위치를 빼 PC 상대 거리를 구합니다. LO12 명령 위치가 아닌 HI20 위치가 두 명령 조합의 기준입니다.

L848 if (IS_ENABLED(CONFIG_MODULE_SECTIONS)

모듈의 GOT·PLT 보조 섹션을 지원하는 빌드인지 확인합니다. 다음 조건까지 만족하면 일반 목적 주소 대신 GOT 항목을 생성하는 경로를 사용합니다.

L849 && hi20_type == R_RISCV_GOT_HI20) {

짝이 GOT_HI20일 때만 GOT 항목을 거치는 주소 계산을 합니다. 일반 PCREL_HI20이면 위에서 계산한 목적 심벌까지의 거리를 그대로 사용합니다.

L850 offset = module_emit_got_entry(

실제 목적값을 담을 모듈 GOT 항목을 만들거나 재사용하고 그 항목의 주소를 얻습니다. 명령은 이 중간 항목을 참조하게 됩니다.

L851 me, hi20_sym_val);

GOT을 소유할 모듈과 항목에 담을 목적 주소를 넘깁니다. 반환값은 그 목적 주소 자체가 아니라 GOT 항목의 위치입니다.

L852 offset = offset - hi20_loc;

GOT 항목 주소에서 HI20 명령 주소를 빼, 명령이 사용할 GOT까지의 PC 상대 거리로 바꿉니다.

L854 hi20 = (offset + 0x800) & 0xfffff000;

하위 12비트가 부호 확장되는 점을 반영해 0x800을 더한 뒤 하위 12비트를 지웁니다. bit 11이 1인 경우 상위 부분을 한 단계 올려야 두 명령의 합이 원래 거리가 됩니다.

L855 lo12 = offset - hi20;

전체 거리에서 조정한 상위 부분을 빼 signed 12비트 하위 즉시값을 얻습니다. 예를 들어 0x1800은 상위 0x2000과 하위 -0x800으로 나뉩니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

PC-relative branch displacement 계산

patch 위치 P=0xffffffffc0201000, target S=0xffffffff81002000, addend A=0이라고 가정한다.

  1. formuladisplacement S+A-P = -0x3f1ff000, 약 -1009.996MiB다.
  2. arm64CALL26 범위 ±128MiB를 벗어나므로 직접 BL encoding이 불가능하고 가까운 module PLT가 필요하다.
  3. x86-64signed rel32 ±2GiB에는 들어가므로 displacement를 32-bit field에 encode할 수 있다.
  4. RISC-VJAL ±1MiB를 벗어난다. 이 거리는 AUIPC+JALR의 CALL/CALL_PLT 도달 범위 안이므로 toolchain이 그 형식으로 만든 호출이라면 직접 encode한다. JAL relocation이 자동으로 long call로 바뀌지는 않는다.

결론relocation 가능 여부는 64-bit 주소를 저장할 수 있느냐가 아니라 해당 instruction immediate의 범위와 alignment로 판단한다.

06 · DEEP DIVE

주요 동작 상세 분석

01

공통 커널 코드와 아키텍처별 함수의 역할 구분

module loader가 ELF section, symbol과 relocation table을 검증하고 core/init layout에 memory를 할당한 뒤 architecture apply_relocate_add()가 S+A-P 등의 식으로 location을 patch한다. 이후 permission을 RX/RO/RW로 강화한다.

untrusted module ELF parsing, writable text patch 구간, instruction-cache publication과 module live state 전환을 분리한다. relocation 실패 시 module init을 호출하기 전에 모든 allocation을 unwind해야 한다.

02

arm64: ADRP/ADD, CALL26/JUMP26와 module PLT

AArch64 relocation은 instruction bitfield에 page-relative 또는 PC-relative immediate를 encode한다. ±128MiB branch range를 벗어나면 module PLT entry가 absolute target을 load해 branch한다.

text patch를 완료한 뒤 D-cache clean과 I-cache invalidate를 수행하고 permission을 executable/read-only로 바꾼다. 디버깅할 때는 relocation type, S/A/P 값, encoded immediate, PLT address, module text range와 icache flush를 확인한다.

03

x86-64: R_X86_64_64, PC32/PLT32와 alternatives

absolute 64-bit와 signed 32-bit PC-relative relocation이 중심이다. module을 kernel text와 가까운 범위에 배치해 rel32 call/jump의 ±2GiB 제한을 맞춘다. 이 x86-64 relocation 함수에는 거리 초과 시 PLT를 만드는 fallback이 없다.

초기 module relocation은 memcpy로 기록하고, 이미 형성된 module에 대한 livepatch relocation은 text_poke와 CPU별 동기화를 사용한다. ORC/ftrace metadata도 relocated address에 맞춰 등록한다. 디버깅할 때는 relocation type, 64-bit result 또는 s32 displacement, P/S/A, module region, ENDBR와 ORC table을 확인한다.

04

RISC-V: HI20/LO12 pair, CALL_PLT, JAL/BRANCH와 relaxation

AUIPC+JALR 또는 LUI+ADDI처럼 여러 instruction이 하나의 symbol address를 구성하므로 paired relocation을 일관되게 적용한다. 먼 target에는 toolchain이 AUIPC+JALR의 CALL 또는 CALL_PLT relocation을 내야 한다. JAL relocation 자체는 PLT로 자동 변환되지 않는다.

instruction pair patch와 icache sync가 끝난 뒤 모든 hart가 새 code를 보도록 remote fence.i 경로를 사용한다. 디버깅할 때는 relocation pair, S/A/P, HI20 rounding, LO12 sign extension, PLT entry와 remote icache synchronization을 본다.

05

객체를 사용하고 해제하는 주체와 시점을 확인한다

module text/data와 symbol/unwind/ftrace metadata는 refcount와 RCU grace period가 끝날 때까지 유지된다. 실행 중인 CPU의 return address가 module text를 가리키면 unload할 수 없다.

주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.

06

최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다

load 시간은 section allocation, relocation 수, symbol lookup, signature/ORC validation과 icache synchronization에 비례한다. runtime hot path에는 직접 branch 또는 PLT indirection 비용이 남는다.

평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64immediate alignment/range overflow, BTI/PAC landing 요구 또는 stale icache는 load 실패와 이전 instruction 실행을 만든다.relocation type, S/A/P 값, encoded immediate, PLT address, module text range와 icache flush를 확인한다.
x86-64rel32 거리 초과, CET IBT ENDBR 누락 또는 metadata 불일치는 잘못된 branch target, control-protection fault와 잘못된 unwind를 만들 수 있다. PC32/PLT32 경로에는 PLT fallback이 없다.relocation type, 64-bit result 또는 s32 displacement, P/S/A, module region, ENDBR와 ORC table을 확인한다.
RISC-VHI20 rounding과 LO12 sign 처리, paired relocation 위치 또는 compressed instruction width 오류가 완전히 다른 target을 만든다.relocation pair, S/A/P, HI20 rounding, LO12 sign extension, PLT entry와 remote icache synchronization을 본다.

08 · LAB

재현과 계측 절차

  1. 작은 test module을 architecture별로 빌드해 readelf -r, objdump -dr와 load 후 runtime address를 비교한다.
  2. target 거리를 인위적으로 늘려 arm64 CALL26의 PLT 생성, RISC-V CALL과 CALL_PLT의 분기, x86-64 PC32/PLT32의 4-byte 기록을 각각 관찰한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinux의 objdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 위치

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.