← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 13/21

ptrace와 hardware debug register

debugger가 task를 정지시키고 register set, breakpoint, single-step 상태를 읽고 쓰는 경로를 architecture regset과 hardware slot 관점에서 비교합니다.

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

01 · QUESTION

어느 경로의 상태를 읽는가

정지된 task의 레지스터는 regset, x86 USER offset, 아키텍처 전용 요청 가운데 어느 경로로 보이는가? hardware watchpoint의 설정과 CPU 설치 시점은 같은 코드가 설명하는가?

아래 세 코드 발췌는 arm64 regset view 선택·MTE 요청 위임, x86 PTRACE_PEEKUSR/POKEUSR의 USER offset 분기, RISC-V의 기본 ptrace_request() 위임을 보여 준다. 이 분기만으로 CPU debug register 설치 시점이나 디버거의 breakpoint 삽입 방식을 결론 내릴 수는 없다.

debug 상태를 읽을 때에는 ABI를 구분한다. arm64 hardware breakpoint/watchpoint는 CONFIG_HAVE_HW_BREAKPOINT 아래의 NT_ARM_HW_BREAK/NT_ARM_HW_WATCH regset을 쓴다 (arm64 ptrace.c:1618–1635). x86 DR0–DR7은 아래 발췌에서 regset이 아니라 USER offset helper로 간다. RISC-V regset 목록에는 GPR과 구성에 따른 FP·vector가 있으며, 여기 인용한 코드에는 trigger CSR ABI가 없다 (RISC-V ptrace.c:187–216).

지연 시간 관점stop/resume 시간, 실제 regset 전송량, breakpoint 이벤트 수를 같은 workload에서 따로 측정한다. 어느 항목이 지연을 지배하는지는 이 소스만으로 단정하지 않는다.

02 · CONTRACT

공통 요청과 서로 다른 레지스터 ABI

architecture이 소스에서 확인한 경로오류·지원 경계확인할 값
arm64user_hwdebug_state의 HW_BREAK/HW_WATCH regset은 CONFIG_HAVE_HW_BREAKPOINT 조건부다 (regset 정의). 아래 arch_ptrace() 발췌는 MTE 태그 요청과 공통 요청 위임을 보여 준다.watchpoint 제어값은 별도 ptrace_hbp_fill_attr_ctrl()에서 변환·검증하고, 지원 slot 수는 hw_breakpoint_slots(TYPE_DATA)로 얻는다 (설정·slot 수). CPU 설치 시점은 이 발췌 범위 밖이다.regset의 resource info, address/control, 요청 반환값을 기록한다.
x86-64DR0–DR7은 PTRACE_PEEKUSR/POKEUSR의 struct user offset과 debugreg helper를 거친다. 일반 레지스터의 regset view는 별도다.정렬·범위가 틀린 USER offset 및 DR4/5 쓰기는 -EIO. DR7 reserved bit는 ptrace_write_dr7()에서 decode 전에 mask하며, breakpoint 구성 실패는 오류로 반환한다 (DR7 helper).요청 종류·offset·반환값과 DR0–3/DR6/DR7 값을 구분한다.
RISC-VGPR regset은 기본, FP·vector regset은 빌드 구성에 따른다 (regset 목록). 아래 arch_ptrace()는 공통 요청으로 위임한다.이 발췌는 trigger CSR, hardware single-step, 디버거가 쓰는 임시 software breakpoint 구현을 보여 주지 않는다. vector regset은 상태가 없으면 -EINVAL을 돌려줄 수 있다 (vector get).커널 구성, 요청 결과, vector 사용 여부와 regset 크기를 기록한다. 하드웨어 watchpoint 지원은 별도 확인한다.

03 · DIAGRAMS

요청·ABI·주소 검사의 세 경계

그림 1. ptrace 요청이 통과하는 세 갈래각 발췌에 실제 등장하는 분기와 위임만 그렸습니다.

arm64 · L2304–2314

MTE 태그 요청
PTRACE_PEEKMTETAGS / POKEMTETAGS → mte_ptrace_copy_tags()
그 밖의 요청
ptrace_request(child, request, addr, data)
별도 관찰
system call 정지의 x7/r12 임시 표지는 L2326–2354에서 처리

x86-64 · L744–778

입력 검사
USER offset의 워드 정렬과 sizeof(struct user) 범위를 검사
GPR
getreg() 또는 putreg() 호출
DR0–DR7
ptrace_get_debugreg() 또는 ptrace_set_debugreg() 호출

RISC-V · L335–351

detach hook
ptrace_disable() 본문은 비어 있음
요청 분기
arch_ptrace()에는 default 하나만 있음
실제 위임
결과를 ptrace_request()에서 받아 그대로 반환
그림 2. 레지스터 보기와 syscall 정지 표지ptrace 요청 처리와 레지스터 ABI가 만나는 지점을 분리합니다.
arm64 syscall-stop 표지 · L2326–2354AArch32는 r12, AArch64는 x7의 원래 값을 저장한 뒤 진입 0/종료 1을 임시로 넣습니다. 일반 진입·종료 보고 뒤 복원하고, 단일 단계 pseudo-step에서는 보고 전에 먼저 복원합니다. 최종 보고 호출은 arm64 ptrace.c:2355–2363에 이어집니다.
arm64 view호출자 AArch32 → user_aarch32_view호출자는 native, 대상만 AArch32 → user_aarch32_ptrace_view; 그 외 → user_aarch64_view (L2296–2301)
x86 native viewCONFIG_X86_64이면 user_x86_64_view그 외 32비트 빌드는 user_x86_32_view를 선택합니다 (L723–742).
RISC-V COMPAT GPRregs_to_cregs() / cregs_to_regs()CONFIG_COMPAT 아래에서 32비트 일반 레지스터 배치를 변환합니다 (L353–381).
ABI 범위이 그림은 저장 레지스터의 보기와 보고 시점을 설명합니다. 실제 CPU debug register의 설치 시점을 뜻하지 않습니다.
그림 3. x86 USER offset에서 debug register까지읽기·쓰기의 같은 주소 검사와 서로 다른 결과를 단계별로 구분합니다.
① offset 검사워드 정렬 실패 또는 addr ≥ sizeof(struct user) → -EIO (L749–751, L765–768)
② GPR / DR 구분addr < sizeof(struct user_regs_struct)면 GPR. U = offsetof(struct user, u_debugreg[0])로 놓으면 DR 배열 범위에서 (addr − U) / sizeof(data)가 번호입니다 (L754–759, L770–776).
③ helper 동작DR0–3 주소, DR6 저장값, DR7 제어값을 구분합니다. DR4/5 쓰기는 -EIO; DR7은 해석 전 reserved bit를 mask합니다. x86 ptrace.c:560–687

04 · SOURCE

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

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

arm64 · Linux 6.18.37

regset view 선택, MTE 요청과 syscall-stop 표지

이 발췌는 ABI별 regset view 선택, MTE 요청 위임과 syscall-stop 표지를 보여 준다. HW_BREAK/HW_WATCH regset과 task에 붙는 perf event는 별도 정의·helper와 조건부 regset 정의에서 확인한다. CPU debug register의 설치 시점은 이 발췌만으로 판정하지 않는다.

원본 코드: arch/arm64/kernel/ptrace.c:2296-2354

2296	if (is_compat_task())
2297		return &user_aarch32_view;
2298	else if (is_compat_thread(task_thread_info(task)))
2299		return &user_aarch32_ptrace_view;
2300 
2301	return &user_aarch64_view;
2302}
2303 
2304long arch_ptrace(struct task_struct *child, long request,
2305		 unsigned long addr, unsigned long data)
2306{
2307	switch (request) {
2308	case PTRACE_PEEKMTETAGS:
2309	case PTRACE_POKEMTETAGS:
2310		return mte_ptrace_copy_tags(child, request, addr, data);
2311	}
2312 
2313	return ptrace_request(child, request, addr, data);
2314}
2315 
2316enum ptrace_syscall_dir {
2317	PTRACE_SYSCALL_ENTER = 0,
2318	PTRACE_SYSCALL_EXIT,
2319};
2320 
2321static void report_syscall(struct pt_regs *regs, enum ptrace_syscall_dir dir)
2322{
2323	int regno;
2324	unsigned long saved_reg;
2325 
2326	/*
2327	 * We have some ABI weirdness here in the way that we handle syscall
2328	 * exit stops because we indicate whether or not the stop has been
2329	 * signalled from syscall entry or syscall exit by clobbering a general
2330	 * purpose register (ip/r12 for AArch32, x7 for AArch64) in the tracee
2331	 * and restoring its old value after the stop. This means that:
2332	 *
2333	 * - Any writes by the tracer to this register during the stop are
2334	 *   ignored/discarded.
2335	 *
2336	 * - The actual value of the register is not available during the stop,
2337	 *   so the tracer cannot save it and restore it later.
2338	 *
2339	 * - Syscall stops behave differently to seccomp and pseudo-step traps
2340	 *   (the latter do not nobble any registers).
2341	 */
2342	regno = (is_compat_task() ? 12 : 7);
2343	saved_reg = regs->regs[regno];
2344	regs->regs[regno] = dir;
2345 
2346	if (dir == PTRACE_SYSCALL_ENTER) {
2347		if (ptrace_report_syscall_entry(regs))
2348			forget_syscall(regs);
2349		regs->regs[regno] = saved_reg;
2350	} else if (!test_thread_flag(TIF_SINGLESTEP)) {
2351		ptrace_report_syscall_exit(regs, 0);
2352		regs->regs[regno] = saved_reg;
2353	} else {
2354		regs->regs[regno] = saved_reg;

주요 구문 해설

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

L2296 if (is_compat_task())

현재 요청을 수행하는 태스크가 AArch32 호환 태스크인지 검사합니다. 이 경우 ptrace나 core dump에 arm32 호환 레지스터 배치를 사용합니다.

L2297 return &user_aarch32_view;

32비트 호출자에 맞는 기본 AArch32 regset 구성을 반환합니다. 레지스터 자료를 arm32 도구가 이해할 폭과 배치로 보여 줍니다.

L2298 else if (is_compat_thread(task_thread_info(task)))

호출자는 native이지만 관찰 대상 task가 AArch32인지 확인합니다. 이 조합에는 기본 호환 보기보다 TLS 접근이 확장된 구성이 필요합니다.

L2299 return &user_aarch32_ptrace_view;

64비트 추적기가 32비트 대상을 볼 때 사용하는 확장 AArch32 ptrace 보기를 반환합니다. TLS 레지스터에도 접근할 수 있는 구성이 포함됩니다.

L2301 return &user_aarch64_view;

위의 32비트 조합이 아니므로 AArch64용 regset 구성을 반환합니다.

L2304long arch_ptrace(struct task_struct *child, long request,

arm64 전용 ptrace 요청을 처리하는 진입점입니다. child는 추적 대상 태스크이며 MTE 태그 요청을 먼저 구분합니다.

L2305 unsigned long addr, unsigned long data)

addr와 data는 요청별 주소·전달 자료를 담습니다. MTE 태그 요청에서는 대상 주소와 태그 복사용 사용자 자료를 처리기로 전달합니다.

L2307 switch (request) {

ptrace 요청 번호로 분기합니다. arm64의 MTE 태그 읽기·쓰기는 전용 처리기로 보내고 나머지는 공통 처리기로 넘깁니다.

L2308 case PTRACE_PEEKMTETAGS:

arm64 MTE tag처럼 공통 regset 바깥의 architecture state를 별도 request로 처리한다.

L2309 case PTRACE_POKEMTETAGS:

메모리 할당 태그를 쓰는 PTRACE_POKEMTETAGS 요청입니다. 바로 앞 읽기 요청과 같은 처리기를 사용하되 request 값으로 방향을 구분합니다.

L2310 return mte_ptrace_copy_tags(child, request, addr, data);

추적 대상의 지정 주소에 있는 MTE 할당 태그를 읽거나 씁니다. request가 방향을 결정하며, 사용자 전달 자료와 복사 결과는 MTE 전용 처리기가 검사합니다.

L2313 return ptrace_request(child, request, addr, data);

architecture 전용 request가 아닌 항목은 공통 ptrace core로 위임한다.

L2316enum ptrace_syscall_dir {

시스템 콜 추적 정지가 진입인지 종료인지 표현할 열거형입니다. 그 값을 ptrace ABI가 지정한 레지스터에 잠시 넣습니다.

L2317 PTRACE_SYSCALL_ENTER = 0,

시스템 콜을 실행하기 전의 추적 정지를 숫자 0으로 나타냅니다.

L2318 PTRACE_SYSCALL_EXIT,

바로 앞 0 다음 값인 1로 시스템 콜 종료 정지를 나타냅니다. 추적기는 이 값으로 진입 정지와 구분합니다.

L2321static void report_syscall(struct pt_regs *regs, enum ptrace_syscall_dir dir)

ptrace에 시스템 콜의 진입 또는 종료를 보고합니다. regs의 특정 레지스터를 임시 표지로 사용한 뒤 원래 값으로 되돌리는 ABI 처리를 포함합니다.

L2323 int regno;

진입·종료 표지를 넣을 레지스터 번호입니다. AArch32는 r12, AArch64는 x7을 선택합니다.

L2324 unsigned long saved_reg;

표지를 덮어쓰기 전 레지스터 값을 보관합니다. 추적 정지 중 이 레지스터를 바꿔도 보고 뒤에는 이 원래 값을 복원합니다.

L2342 regno = (is_compat_task() ? 12 : 7);

ptrace에 시스템 콜 진입·종료 방향을 잠시 표시할 레지스터를 고릅니다. 32비트 호환 태스크는 r12, AArch64 태스크는 x7을 쓰며, 원래 값은 저장했다가 보고 후 되돌립니다.

L2343 saved_reg = regs->regs[regno];

r12 또는 x7의 원래 값을 saved_reg에 보관합니다. 다음 줄의 진입·종료 표지가 실제 사용자 값을 영구히 바꾸지 않게 합니다.

L2344 regs->regs[regno] = dir;

선택한 레지스터에 진입 0 또는 종료 1을 넣어 추적기가 정지 종류를 알아보게 합니다. 보고가 끝난 뒤 이 값은 원래 값으로 복구됩니다.

L2346 if (dir == PTRACE_SYSCALL_ENTER) {

시스템 콜 진입 정지인지 구분합니다. 진입 경로에서는 추적기 판단에 따라 실제 시스템 콜 실행을 생략할 수도 있습니다.

L2347 if (ptrace_report_syscall_entry(regs))

진입 정지를 보고한 결과 시스템 콜을 실행하지 않아야 하는지 확인합니다. 참이면 다음 줄이 syscallno를 NO_SYSCALL로 바꿉니다.

L2348 forget_syscall(regs);

추적기가 이 시스템 콜의 실행을 취소하도록 요청한 경우 regs->syscallno를 NO_SYSCALL로 바꿉니다. 이후 디스패치가 원래 번호의 시스템 콜을 실행하지 않도록 남기는 표시입니다.

L2349 regs->regs[regno] = saved_reg;

진입 보고가 끝났으므로 임시 방향 표지 대신 원래 사용자 레지스터 값을 되돌립니다.

L2350 } else if (!test_thread_flag(TIF_SINGLESTEP)) {

종료 보고이면서 단일 단계 추적이 아닌지 검사합니다. 일반 종료 정지는 임시 표지를 유지한 채 보고하고, 단일 단계는 별도 경로로 처리합니다.

L2351 ptrace_report_syscall_exit(regs, 0);

시스템 콜 종료 지점을 ptrace 추적기에 보고합니다. 두 번째 인자 0은 단일 단계 실행에 따른 가상 종료 보고가 아닌 일반 시스템 콜 종료 정지임을 나타냅니다.

L2352 regs->regs[regno] = saved_reg;

일반 종료 정지를 추적기에 보고한 뒤 레지스터를 원래 값으로 복원합니다.

L2353 } else {

종료 시 단일 단계 실행 중인 경로입니다. pseudo-step 보고는 일반 시스템 콜 정지와 달리 레지스터를 덮은 상태로 보여 주지 않습니다.

L2354 regs->regs[regno] = saved_reg;

pseudo-step 보고 전에 원래 레지스터를 먼저 복구합니다. 아래 ptrace_report_syscall_exit(regs, 1)이 원래 레지스터 상태로 단일 단계 정지를 알리게 합니다.

x86-64 · Linux 6.18.37

USER offset의 GPR·DR 분기

표시된 PTRACE_PEEKUSR/PTRACE_POKEUSR 분기는 struct user의 GPR 또는 DR0–DR7 offset을 각각 해당 helper에 전달한다. xstate regset은 이 USER offset 경로와 별개다. DR7 제어값은 별도 helper에서 reserved bit를 mask한 뒤 breakpoint slot을 해석한다.

원본 코드: arch/x86/kernel/ptrace.c:722-780

722 
723#if defined CONFIG_X86_32 || defined CONFIG_IA32_EMULATION
724static const struct user_regset_view user_x86_32_view; /* Initialized below. */
725#endif
726#ifdef CONFIG_X86_64
727static const struct user_regset_view user_x86_64_view; /* Initialized below. */
728#endif
729 
730long arch_ptrace(struct task_struct *child, long request,
731		 unsigned long addr, unsigned long data)
732{
733	int ret;
734	unsigned long __user *datap = (unsigned long __user *)data;
735 
736#ifdef CONFIG_X86_64
737	/* This is native 64-bit ptrace() */
738	const struct user_regset_view *regset_view = &user_x86_64_view;
739#else
740	/* This is native 32-bit ptrace() */
741	const struct user_regset_view *regset_view = &user_x86_32_view;
742#endif
743 
744	switch (request) {
745	/* read the word at location addr in the USER area. */
746	case PTRACE_PEEKUSR: {
747		unsigned long tmp;
748 
749		ret = -EIO;
750		if ((addr & (sizeof(data) - 1)) || addr >= sizeof(struct user))
751			break;
752 
753		tmp = 0;  /* Default return condition */
754		if (addr < sizeof(struct user_regs_struct))
755			tmp = getreg(child, addr);
756		else if (addr >= offsetof(struct user, u_debugreg[0]) &&
757			 addr <= offsetof(struct user, u_debugreg[7])) {
758			addr -= offsetof(struct user, u_debugreg[0]);
759			tmp = ptrace_get_debugreg(child, addr / sizeof(data));
760		}
761		ret = put_user(tmp, datap);
762		break;
763	}
764 
765	case PTRACE_POKEUSR: /* write the word at location addr in the USER area */
766		ret = -EIO;
767		if ((addr & (sizeof(data) - 1)) || addr >= sizeof(struct user))
768			break;
769 
770		if (addr < sizeof(struct user_regs_struct))
771			ret = putreg(child, addr, data);
772		else if (addr >= offsetof(struct user, u_debugreg[0]) &&
773			 addr <= offsetof(struct user, u_debugreg[7])) {
774			addr -= offsetof(struct user, u_debugreg[0]);
775			ret = ptrace_set_debugreg(child,
776						  addr / sizeof(data), data);
777		}
778		break;
779 
780	case PTRACE_GETREGS:	/* Get all gp regs from the child. */

주요 구문 해설

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

L723#if defined CONFIG_X86_32 || defined CONFIG_IA32_EMULATION

32비트 커널이거나 64비트 커널에서 IA32 호환 실행을 지원하면 32비트 레지스터 보기 형식을 선언합니다. ptrace가 32비트 프로그램의 레지스터 폭과 배치를 정확히 다루기 위한 정의입니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L724static const struct user_regset_view user_x86_32_view; /* Initialized below. */

32비트 x86 레지스터의 배치와 읽기·쓰기 함수를 묶은 regset 보기의 선언입니다. 실제 항목 초기화는 파일 아래에 있습니다.

L725#endif

723행에서 시작한 선택 구간을 마칩니다. 32비트 커널이거나 64비트 커널에서 IA32 호환 실행을 지원하면 32비트 레지스터 보기 형식을 선언합니다. ptrace가 32비트 프로그램의 레지스터 폭과 배치를 정확히 다루기 위한 정의입니다.

L726#ifdef CONFIG_X86_64

64비트 x86 커널용 레지스터 보기 형식을 선언합니다. 32비트 사용자 레지스터 집합과 크기·구성이 달라 별도의 user_regset_view를 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L727static const struct user_regset_view user_x86_64_view; /* Initialized below. */

64비트 x86용 regset 보기의 선언입니다. 아래 arch_ptrace가 native 64비트 요청을 처리할 때 이 구성을 고릅니다.

L728#endif

726행에서 시작한 선택 구간을 마칩니다. 64비트 x86 커널용 레지스터 보기 형식을 선언합니다. 32비트 사용자 레지스터 집합과 크기·구성이 달라 별도의 user_regset_view를 사용합니다.

L730long arch_ptrace(struct task_struct *child, long request,

x86의 ptrace 요청을 처리합니다. 추적 대상 child의 일반·디버그 레지스터 등 요청별 자료를 읽거나 바꿉니다.

L731 unsigned long addr, unsigned long data)

addr는 USER 영역 오프셋 등 요청 대상 위치이고 data는 쓸 값 또는 결과 버퍼 주소입니다. 어느 의미인지는 request가 결정합니다.

L733 int ret;

ptrace 요청의 성공·오류 결과를 저장할 ret를 선언합니다. 뒤의 요청별 분기에서 값을 정한 후 호출자에게 전달하며 이 줄은 추적 대상의 CPU 상태를 복원하지 않습니다.

L734 unsigned long __user *datap = (unsigned long __user *)data;

data를 사용자 공간의 워드 포인터로 해석합니다. PEEKUSR 같은 읽기 요청의 결과를 추적기 메모리에 써 줄 때 사용합니다.

L736#ifdef CONFIG_X86_64

네이티브 ptrace 요청의 레지스터 보기 형식을 커널 비트수에 맞춥니다. 64비트 빌드는 user_x86_64_view를 선택하고 32비트 빌드는 아래 분기의 user_x86_32_view를 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L738 const struct user_regset_view *regset_view = &user_x86_64_view;

64비트 커널의 native ptrace 요청이므로 64비트 레지스터 배치를 선택합니다. 이어지는 GETREGS 등의 전체 레지스터 복사에서 사용합니다.

L739#else

네이티브 32비트 ptrace 요청이므로 user_x86_32_view를 사용합니다. 디버거에 보일 레지스터의 크기와 배치를 32비트 사용자 ABI에 맞춥니다.

L741 const struct user_regset_view *regset_view = &user_x86_32_view;

32비트 커널의 native 요청에는 32비트 레지스터 배치를 선택합니다. 64비트 커널의 별도 호환 처리 경로와는 다른 전처리 분기입니다.

L742#endif

736행에서 시작한 선택 구간을 마칩니다. 네이티브 ptrace 요청의 레지스터 보기 형식을 커널 비트수에 맞춥니다. 64비트 빌드는 user_x86_64_view를 선택하고 32비트 빌드는 아래 분기의 user_x86_32_view를 사용합니다.

L744 switch (request) {

request 값으로 USER 영역 한 워드 접근, 전체 레지스터 접근 등의 ptrace 작업을 구분합니다.

L746 case PTRACE_PEEKUSR: {

전통적인 USER 영역에서 한 워드를 읽는 요청입니다. 실제 위치에 따라 일반 레지스터 또는 디버그 레지스터 저장 상태에서 값을 가져옵니다.

L747 unsigned long tmp;

추적기에 돌려줄 한 워드 값을 임시 보관합니다. 유효한 레지스터 위치이면 읽은 값으로, 그 외 허용된 USER 빈 영역이면 0으로 채웁니다.

L749 ret = -EIO;

USER 영역 읽기의 기본 결과를 -EIO로 둡니다. addr가 워드 경계에 맞지 않거나 struct user 범위 밖이면 다음 검사에서 빠져나가 이 오류를 반환합니다.

L750 if ((addr & (sizeof(data) - 1)) || addr >= sizeof(struct user))

오프셋이 워드 크기로 정렬되지 않았거나 struct user 밖이면 접근을 거절합니다. 올바른 레지스터 필드 경계로만 읽도록 제한합니다.

L751 break;

잘못된 USER 오프셋이므로 switch를 빠져나갑니다. 위에서 준비한 -EIO가 결과로 유지됩니다.

L753 tmp = 0; /* Default return condition */

유효 범위이지만 일반·디버그 레지스터에 해당하지 않는 USER 영역은 0을 읽은 것으로 처리하도록 기본값을 둡니다.

L754 if (addr < sizeof(struct user_regs_struct))

USER 영역 앞쪽의 일반 레지스터 구조체 안인지 확인합니다. 해당하면 getreg로 대상 태스크의 저장 레지스터를 읽습니다.

L755 tmp = getreg(child, addr);

PTRACE_PEEKUSR가 요청한 user 영역 오프셋 addr에 대응하는 자식 태스크의 레지스터 값을 읽어 tmp에 둡니다. 실제 CPU의 현재 레지스터가 아니라 추적 대상에 저장된 레지스터 상태를 조회합니다.

L756 else if (addr >= offsetof(struct user, u_debugreg[0]) &&

일반 레지스터가 아니라면 디버그 레지스터 배열의 시작 이상인지 확인합니다. 다음 조건과 함께 DR0부터 DR7 사이만 허용합니다.

L757 addr <= offsetof(struct user, u_debugreg[7])) {

오프셋이 마지막 디버그 레지스터 DR7을 넘지 않는지도 검사합니다. 두 범위 조건을 만족할 때만 디버그 레지스터 번호로 바꿔 읽습니다.

L758 addr -= offsetof(struct user, u_debugreg[0]);

addr에서 struct user 안의 u_debugreg 배열 시작 오프셋을 뺍니다. 구조체 전체 기준 바이트 위치를 디버그 레지스터 배열 내부의 위치로 바꾸는 계산이며, offsetof는 실행 함수가 아닙니다.

L759 tmp = ptrace_get_debugreg(child, addr / sizeof(data));

배열 내부 바이트 오프셋을 한 항목의 크기로 나누어 DR 번호를 구하고, 해당 디버그 레지스터의 저장 상태를 tmp로 읽습니다. 결과는 아래 put_user를 통해 추적기에 전달됩니다.

L761 ret = put_user(tmp, datap);

추적 대상에서 얻은 tmp 값을 추적자 프로세스의 사용자 주소 datap에 기록합니다. 복사가 성공하면 ret는 0, 잘못된 사용자 주소로 실패하면 -EFAULT가 됩니다.

L762 break;

읽은 워드의 사용자 버퍼 복사를 마치고 요청 분기를 종료합니다. put_user가 정한 성공 또는 접근 오류가 ret에 남습니다.

L765 case PTRACE_POKEUSR: /* write the word at location addr in the USER area */

USER 영역의 한 워드를 새 값 data로 바꾸는 요청입니다. 아래에서 일반 레지스터와 디버그 레지스터 쓰기를 구분합니다.

L766 ret = -EIO;

USER 영역 쓰기의 기본 결과를 -EIO로 설정합니다. 뒤의 정렬·범위 검사와 지원 레지스터 선택을 통과한 경우에만 실제 쓰기 결과로 바뀝니다.

L767 if ((addr & (sizeof(data) - 1)) || addr >= sizeof(struct user))

쓰기 위치가 워드 정렬을 지키고 struct user 범위 안에 있는지 검사합니다. 잘못된 위치에 레지스터 값을 쓰지 않게 합니다.

L768 break;

잘못된 쓰기 오프셋이므로 레지스터를 수정하지 않고 분기를 끝냅니다. 결과는 기본 오류 -EIO입니다.

L770 if (addr < sizeof(struct user_regs_struct))

일반 레지스터 구조체의 위치이면 putreg로 해당 레지스터를 갱신합니다. 세그먼트나 플래그 등 필드별 제한은 그 처리기가 검사합니다.

L771 ret = putreg(child, addr, data);

USER 영역의 일반 레지스터 오프셋에 data를 써 추적 대상의 저장 레지스터 상태를 갱신합니다. putreg가 세그먼트·flags·FS/GS base 등의 제약도 처리하며 성공 여부를 ret에 돌려줍니다.

L772 else if (addr >= offsetof(struct user, u_debugreg[0]) &&

일반 레지스터 밖의 요청이 디버그 레지스터 배열 시작 이상인지 검사합니다.

L773 addr <= offsetof(struct user, u_debugreg[7])) {

DR7 위치 이하인 요청만 디버그 레지스터 쓰기로 처리합니다. 허용 범위를 벗어나면 ret의 기본 오류값이 유지됩니다.

L774 addr -= offsetof(struct user, u_debugreg[0]);

쓰기 요청의 addr를 u_debugreg 배열 내부 바이트 오프셋으로 바꿉니다. 이어서 sizeof(data)로 나눈 값이 변경할 디버그 레지스터 번호가 됩니다.

L775 ret = ptrace_set_debugreg(child,

추적 대상의 디버그 레지스터 설정 함수를 호출하는 첫 줄입니다. 다음 줄의 addr/sizeof(data)가 DR 번호, data가 새 값이며 breakpoint 주소나 DR6·DR7 상태를 검사·갱신한 결과를 ret에 받습니다.

L776 addr / sizeof(data), data);

앞줄의 ptrace_set_debugreg 호출에 디버그 레지스터 번호와 새 값 data를 전달하는 부분입니다. 중단점 관련 제약을 검사하며 추적 대상 상태를 갱신하고, 성공 또는 오류 코드는 ret로 받습니다.

L778 break;

일반 또는 디버그 레지스터 쓰기를 마치고 분기를 끝냅니다. 실제 쓰기 처리기가 돌려준 성공·오류가 최종 결과가 됩니다.

L780 case PTRACE_GETREGS: /* Get all gp regs from the child. */

일반 레지스터 전체를 요청한 경우입니다. 선택한 native regset 보기의 REGSET_GENERAL 자료를 사용자 버퍼로 복사하는 경로로 이어집니다.

RISC-V · Linux 6.18.37

공통 ptrace 요청 위임과 compat GPR 변환

이 발췌의 arch_ptrace()는 요청을 공통 ptrace_request()에 넘기며, 아래 compat 코드는 GPR 배치를 변환한다. 별도 regset 목록은 GPR과 구성 조건부 FP/vector를 정의한다. 이 범위에서 trigger CSR, hardware single-step 또는 디버거의 임시 software breakpoint 삽입은 확인되지 않는다.

원본 코드: arch/riscv/kernel/ptrace.c:331-381

331	else
332		return 0;
333}
334 
335void ptrace_disable(struct task_struct *child)
336{
337}
338 
339long arch_ptrace(struct task_struct *child, long request,
340		 unsigned long addr, unsigned long data)
341{
342	long ret = -EIO;
343 
344	switch (request) {
345	default:
346		ret = ptrace_request(child, request, addr, data);
347		break;
348	}
349 
350	return ret;
351}
352 
353#ifdef CONFIG_COMPAT
354static int compat_riscv_gpr_get(struct task_struct *target,
355				const struct user_regset *regset,
356				struct membuf to)
357{
358	struct compat_user_regs_struct cregs;
359 
360	regs_to_cregs(&cregs, task_pt_regs(target));
361 
362	return membuf_write(&to, &cregs,
363			    sizeof(struct compat_user_regs_struct));
364}
365 
366static int compat_riscv_gpr_set(struct task_struct *target,
367				const struct user_regset *regset,
368				unsigned int pos, unsigned int count,
369				const void *kbuf, const void __user *ubuf)
370{
371	int ret;
372	struct compat_user_regs_struct cregs;
373 
374	ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &cregs, 0, -1);
375 
376	cregs_to_regs(&cregs, task_pt_regs(target));
377 
378	return ret;
379}
380 
381static const struct user_regset compat_riscv_user_regset[] = {

주요 구문 해설

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

L331 else

요청한 n번째 워드가 저장된 SP와 같은 커널 스택 범위를 벗어난 경우입니다. 임의 커널 주소를 역참조하지 않고 실패값을 돌려줍니다.

L332 return 0;

요청 위치가 해당 커널 스택 밖이므로 0을 반환합니다. 주소 0에서 읽는 동작이 아니라 범위 밖 접근을 거절한 결과입니다.

L335void ptrace_disable(struct task_struct *child)

추적 해제 시 아키텍처별 정리를 수행할 자리입니다. 이 버전 RISC-V 구현은 빈 함수이므로 child의 레지스터나 추적 상태를 추가 변경하지 않습니다.

L339long arch_ptrace(struct task_struct *child, long request,

RISC-V의 ptrace 진입점입니다. 이 함수에는 독자적인 요청 case가 없어 실제 요청 처리는 공통 ptrace_request가 맡습니다.

L340 unsigned long addr, unsigned long data)

추적 대상에 적용할 주소와 자료 값을 받습니다. 이 함수에서 별도 변환 없이 요청 번호와 함께 공통 처리기로 전달합니다.

L342 long ret = -EIO;

ptrace 처리 결과를 담을 long 변수를 -EIO로 초기화합니다. 이 함수의 현재 switch는 모든 요청을 아래의 공통 처리기로 넘기므로 실제 결과로 다시 대입됩니다.

L344 switch (request) {

요청 번호를 구분할 switch이지만 현재는 default만 있습니다. 모든 요청이 아래의 공통 ptrace 처리로 이어집니다.

L345 default:

모든 요청에 적용되는 기본 분기입니다. 공통 ptrace가 지원 여부와 필요한 자료 접근을 판단합니다.

L346 ret = ptrace_request(child, request, addr, data);

추적 대상 child와 요청 종류·주소·데이터를 공통 ptrace 처리기에 넘깁니다. RISC-V 전용 분기가 없는 요청의 실제 처리 결과를 ret에 저장합니다.

L347 break;

공통 처리기가 반환한 결과를 ret에 받은 뒤 switch를 나갑니다. 함수 아래에서 그 결과를 그대로 반환합니다.

L350 return ret;

공통 ptrace 처리기가 반환한 결과를 그대로 돌려줍니다. 반환값은 요청의 성공·오류 상태이며 privilege나 인터럽트 마스크를 복원하는 명령이 아닙니다.

L353#ifdef CONFIG_COMPAT

COMPAT은 64비트 커널에서 32비트 사용자 ABI를 지원하는 구성입니다. 켜지면 64비트 내부 pt_regs를 호환 레지스터 구조체로 바꿔 읽고 쓰는 ptrace helper를 포함하며, 꺼지면 그 별도 변환 경로를 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L354static int compat_riscv_gpr_get(struct task_struct *target,

호환 태스크의 일반 레지스터를 읽어 32비트 ABI 형식으로 내보내는 regset 콜백입니다. target은 레지스터를 읽을 태스크입니다.

L355 const struct user_regset *regset,

어떤 regset 항목을 요청했는지 설명하는 메타데이터를 받습니다. 이 콜백의 본문은 고정된 호환 일반 레지스터 배치를 사용합니다.

L356 struct membuf to)

변환한 레지스터 자료를 쓸 membuf 출력 상태를 받습니다. 남은 버퍼 범위에 맞춘 복사는 membuf_write가 담당합니다.

L358 struct compat_user_regs_struct cregs;

커널 pt_regs를 32비트 호환 형식으로 변환해 담을 임시 구조체입니다. 변환한 자료를 그대로 출력 버퍼로 보냅니다.

L360 regs_to_cregs(&cregs, task_pt_regs(target));

추적 대상의 커널 pt_regs를 32비트 호환 ABI가 기대하는 compat_user_regs_struct로 변환합니다. 커널 내부 레지스터 표현을 그대로 내보내지 않고 호환 프로세스의 자료 폭과 배치에 맞춥니다.

L362 return membuf_write(&to, &cregs,

변환한 cregs를 출력 버퍼에 쓰고 membuf_write의 결과를 반환합니다. 뒤의 크기 인자는 호환 레지스터 구조체 전체 길이입니다.

L363 sizeof(struct compat_user_regs_struct));

앞줄의 membuf_write에 복사할 32비트 호환 레지스터 구조체의 전체 크기를 넘깁니다. 변환된 cregs를 regset 출력 버퍼에 쓰는 작업의 길이이며, 별도의 함수 호출은 아닙니다.

L366static int compat_riscv_gpr_set(struct task_struct *target,

호환 ABI로 받은 일반 레지스터 값을 대상 태스크에 반영하는 regset 쓰기 콜백입니다.

L367 const struct user_regset *regset,

호환 일반 레지스터 항목의 메타데이터를 받습니다. 실제 변환에는 compat_user_regs_struct 배치를 사용합니다.

L368 unsigned int pos, unsigned int count,

입력 자료의 시작 바이트 위치 pos와 남은 바이트 수 count를 받습니다. user_regset_copyin이 이 범위에 맞춰 입력을 가져옵니다.

L369 const void *kbuf, const void __user *ubuf)

입력이 커널 버퍼에 있으면 kbuf, 사용자 버퍼에 있으면 ubuf로 받습니다. 공통 regset 복사기가 적절한 쪽에서 읽습니다.

L371 int ret;

호환 모드 레지스터 자료를 복사한 결과를 저장할 int 변수를 선언합니다. 대상 레지스터 집합은 다음의 cregs에 준비합니다.

L372 struct compat_user_regs_struct cregs;

입력받은 32비트 레지스터 묶음을 임시 보관할 구조체입니다. 이후 cregs_to_regs가 대상 pt_regs의 내부 형식으로 변환합니다.

L374 ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &cregs, 0, -1);

호출자가 지정한 pos·count 구간의 자료를 커널 버퍼 kbuf 또는 사용자 버퍼 ubuf에서 호환 모드 레지스터 구조체 cregs로 복사합니다. 복사 위치와 남은 길이를 갱신하고 성공·오류 결과를 ret에 받습니다.

L376 cregs_to_regs(&cregs, task_pt_regs(target));

입력받은 호환 레지스터 구조체 cregs를 추적 대상의 pt_regs 표현으로 되돌려 반영합니다. 바로 앞의 복사 결과는 ret로 따로 반환하며, 이 줄 앞에 성공 여부를 검사하는 분기가 있는 것은 아닙니다.

L378 return ret;

호환 모드 레지스터 입력의 복사 결과를 반환합니다. 앞에서 cregs_to_regs로 대상 pt_regs에 값을 옮긴 뒤의 C 함수 반환이며, 이 줄에서 대상 태스크를 실행시키지는 않습니다.

L381static const struct user_regset compat_riscv_user_regset[] = {

호환 프로세스가 공개하는 regset 항목 배열입니다. 각 항목이 자료 크기·정렬과 읽기·쓰기 콜백을 연결하여 ptrace와 core dump에 같은 배치를 제공합니다.

05 · WORKED EXAMPLE

arm64 watchpoint 설정 경로 검산

01

주소 0x7fff2000을 감시하고 싶다면

주소는 예시다. 요청한 길이·접근 형식과 slot 수는 실행 중인 커널과 CPU에서 검증해야 하며, 여기서는 CPU0에서 CPU3으로 옮겨 갈 때의 레지스터 복원을 가정하지 않는다.

  1. 지원 확인NT_ARM_HW_WATCH regset의 resource info는 hw_breakpoint_slots(TYPE_DATA)로 얻은 slot 수를 포함한다 (slot 수).
  2. 주소·제어 입력hw_break_set()은 regset의 address/control 쌍을 순서대로 읽어 ptrace_hbp_set_addr()/ptrace_hbp_set_ctrl()로 전달한다 (regset 쓰기).
  3. task 상태필요하면 register_user_hw_breakpoint(..., tsk)로 perf event를 만들고 tsk->thread.debug.hbp_watch[]에 둔다. 제어값은 arch_bp_generic_fields()와 modify_user_hw_breakpoint() 경로에서 검증·변경한다 (event 생성·필드 변환, 제어값 검증·반영).
  4. 적중·해제적중 callback은 native task에 SIGTRAP/TRAP_HWBKPT를 전달한다. flush_ptrace_hw_breakpoint()는 event를 등록 해제하고 task 포인터를 비운다 (callback·해제).

소스 경계이 예제는 arm64 ptrace의 ABI·task 상태·callback을 따라간다. CPU별 debug register 설치와 migration 동작은 별도 perf/hw_breakpoint 경로와 실측으로 확인해야 한다.

06 · DEEP DIVE

소스에 나타난 동작과 별도 검증할 동작

01

regset과 USER offset을 혼동하지 않는다

arm64의 task_user_regset_view()는 호출자·대상 ABI에 따라 view를 고른다. x86 USER offset 분기는 struct user의 GPR 또는 DR 위치를 검사해 각 helper로 보낸다. RISC-V의 arch_ptrace()는 현재 표시한 분기에서 공통 ptrace_request()에 넘긴다.

따라서 세 아키텍처의 모든 debug register가 같은 ELF note regset으로 노출된다고 쓰지 않는다. arm64 조건부 HW_BREAK/HW_WATCH regset, x86 DR USER offset, RISC-V의 표시된 GPR·호환 변환은 서로 다른 근거다.

02

arm64: 조건부 HW_BREAK/HW_WATCH regset

표시된 ptrace.c:2296–2314는 regset view 선택과 MTE 전용 요청·공통 요청 위임만 보여 준다. hardware debug ABI는 별도 REGSET_HW_BREAK/REGSET_HW_WATCH 정의와 hw_break_get()/hw_break_set()에서 확인한다 (regset helper, regset 정의).

event는 register_user_hw_breakpoint(..., tsk)로 생성하고 task의 hbp_break[]/hbp_watch[]에서 찾는다 (event 등록). CPU debug register 재설치, OS lock, monitor enable, PSTATE.SS의 구체적 시점은 이 발췌에서 추론하지 않는다.

03

x86-64: USER offset의 DR0–DR7

PTRACE_PEEKUSR/POKEUSR는 워드 정렬과 sizeof(struct user) 범위를 먼저 확인한다. DR 영역이면 ptrace_get_debugreg()/ptrace_set_debugreg()로 간다. 이 경로는 xstate regset과 구분된다 (USER offset 분기).

DR4/5 쓰기는 -EIO다. DR7 쓰기 helper는 DR_CONTROL_RESERVED를 mask한 뒤 slot을 decode·구성하고 오류 시 이전 breakpoint 구성을 복구하려 한다 (DR7 helper, DR4–DR7 분기). 이 코드만으로 임의 입력이 #DB loop를 만든다고 단정하지 않는다.

04

RISC-V: regset 목록과 공통 요청 위임

riscv_user_regset[]은 GPR을 정의하고 CONFIG_FPU/CONFIG_RISCV_ISA_V에 따라 FP/vector 항목을 더한다 (regset 목록). vector get은 대상 상태를 확인하며, target == current일 때 저장 후 복사한다 (vector get). 표시된 arch_ptrace()에는 default 위임만 있고 compat 부분은 GPR 형식을 변환한다.

여기에는 trigger CSR 접근, hardware single-step, compressed instruction을 고려한 software breakpoint 삽입 코드가 없다. 그런 동작은 디버거 구현과 해당 커널의 별도 지원 경로를 확인한 뒤에만 설명할 수 있다.

05

task에 붙는 상태와 CPU 설치를 분리한다

arm64에서 ptrace hardware breakpoint perf event는 tsk->thread.debug.hbp_break[]/hbp_watch[]에 연결되며 flush_ptrace_hw_breakpoint()에서 등록 해제된다 (task event 수명). x86 helper는 tsk->thread.ptrace_bps[]의 breakpoint event를 읽고 수정한다 (x86 event helper).

이 task 수준의 소유 관계가 곧 세 아키텍처에 동일한 CPU slot 수명 규칙을 뜻하지는 않는다. CPU register에 언제 설치·제거되는지는 각 아키텍처의 hw_breakpoint/perf 경로를 따로 추적해야 한다.

06

지연은 요청별로 계측한다

같은 대상과 workload에서 ptrace stop/resume, regset 전송 byte 수, breakpoint callback·SIGTRAP 빈도를 따로 기록한다. vector 상태 저장이 필요한 경우와 그렇지 않은 경우도 분리한다 (RISC-V vector get).

평균뿐 아니라 각 단계의 최대값과 CPU 이동 여부를 기록한다. 펌웨어 호출 시간·재시도 횟수는 이 ptrace 소스에 나타나지 않으므로, 별도 펌웨어 경로가 실제 workload에 있음을 입증하지 않았다면 여기의 지연 항목으로 넣지 않는다.

07 · FAILURE

오류를 어떤 경로에서 확인할 것인가

분류관찰되는 결과·경계첫 확인값
arm64HW_BREAK/HW_WATCH regset은 빌드 조건부다. control 변환·event 수정에서 오류가 반환될 수 있다. 아래 MTE 요청 분기는 hardware watchpoint 검증 자체를 보여 주지 않는다.커널 구성, regset resource info의 slot 수, address/control, ptrace_hbp_fill_attr_ctrl() 반환값 (검증·slot 수).
x86-64USER offset 정렬·범위 실패 및 DR4/5 쓰기는 -EIO. DR7의 reserved bit는 helper가 decode 전에 mask하므로, 이를 그대로 hardware에 전달한다고 해석하면 안 된다. breakpoint 재구성 오류는 호출자에게 반환된다.요청 offset과 반환값, ptrace_write_dr7()의 mask·decode·복구 경로 (DR7 helper).
RISC-V표시된 arch_ptrace()는 요청을 공통 처리기로 넘긴다. vector regset은 상태가 없거나 쓰기 때 vlenb가 맞지 않으면 -EINVAL일 수 있다. trigger/stepping 가능 여부는 이 소스로 판정하지 않는다.커널 구성과 regset note type, vector 활성 여부·vlenb, 요청 반환값 (vector get/set).

08 · LAB

지원 여부부터 확인하는 계측

  1. arm64는 실행 커널의 CONFIG_HAVE_HW_BREAKPOINT와 NT_ARM_HW_WATCH resource info를, x86은 DR USER offset 요청의 반환값을 먼저 기록한다. GDB watchpoint와 task affinity 변경 실험은 실제 slot·권한·디버거 지원이 확인된 환경에서만 한다.
  2. RISC-V hardware watchpoint나 single-step 실험은 해당 커널과 디버거가 그 경로를 제공한다는 별도 근거를 확보한 경우에만 진행한다. 이 글의 arch_ptrace() 발췌만으로 지원을 가정하지 않는다.
  3. FP/vector regset은 커널 구성과 대상 상태를 기록한 뒤 읽기 전후 시간·전송 byte 수·반환값을 측정한다. RISC-V vector는 비활성 상태의 -EINVAL과 vlenb 불일치를 따로 기록한다.
  4. 동일 workload에서 ptrace 요청 종류, stop/resume 시각, CPU 번호, PC와 오류 코드를 같은 표에 기록한다. CPU별 debug register 설치 여부는 별도 hw_breakpoint/perf 추적 결과로 판정한다.

09 · REFERENCES

원문 위치

Linux kernel source: GPL-2.0-only. 표시된 코드와 별도 참고 코드 모두 Linux v6.18.37을 가리킵니다. debugger 구현·CPU debug register 설치와 같은 별도 경로는 이 글의 발췌만으로 결론 내리지 않습니다.