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).
02 · CONTRACT
공통 요청과 서로 다른 레지스터 ABI
| architecture | 이 소스에서 확인한 경로 | 오류·지원 경계 | 확인할 값 |
|---|---|---|---|
| arm64 | user_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-64 | DR0–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-V | GPR 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·주소 검사의 세 경계
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()에서 받아 그대로 반환
user_aarch32_view호출자는 native, 대상만 AArch32 → user_aarch32_ptrace_view; 그 외 → user_aarch64_view (L2296–2301)CONFIG_X86_64이면 user_x86_64_view그 외 32비트 빌드는 user_x86_32_view를 선택합니다 (L723–742).regs_to_cregs() / cregs_to_regs()CONFIG_COMPAT 아래에서 32비트 일반 레지스터 배치를 변환합니다 (L353–381).addr ≥ sizeof(struct user) → -EIO (L749–751, L765–768)addr < sizeof(struct user_regs_struct)면 GPR. U = offsetof(struct user, u_debugreg[0])로 놓으면 DR 배열 범위에서 (addr − U) / sizeof(data)가 번호입니다 (L754–759, L770–776).-EIO; DR7은 해석 전 reserved bit를 mask합니다. x86 ptrace.c:560–68704 · 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개 구문에 설명을 붙였습니다.
if (is_compat_task())현재 요청을 수행하는 태스크가 AArch32 호환 태스크인지 검사합니다. 이 경우 ptrace나 core dump에 arm32 호환 레지스터 배치를 사용합니다.
return &user_aarch32_view;32비트 호출자에 맞는 기본 AArch32 regset 구성을 반환합니다. 레지스터 자료를 arm32 도구가 이해할 폭과 배치로 보여 줍니다.
else if (is_compat_thread(task_thread_info(task)))호출자는 native이지만 관찰 대상 task가 AArch32인지 확인합니다. 이 조합에는 기본 호환 보기보다 TLS 접근이 확장된 구성이 필요합니다.
return &user_aarch32_ptrace_view;64비트 추적기가 32비트 대상을 볼 때 사용하는 확장 AArch32 ptrace 보기를 반환합니다. TLS 레지스터에도 접근할 수 있는 구성이 포함됩니다.
return &user_aarch64_view;위의 32비트 조합이 아니므로 AArch64용 regset 구성을 반환합니다.
long arch_ptrace(struct task_struct *child, long request,arm64 전용 ptrace 요청을 처리하는 진입점입니다. child는 추적 대상 태스크이며 MTE 태그 요청을 먼저 구분합니다.
unsigned long addr, unsigned long data)addr와 data는 요청별 주소·전달 자료를 담습니다. MTE 태그 요청에서는 대상 주소와 태그 복사용 사용자 자료를 처리기로 전달합니다.
switch (request) {ptrace 요청 번호로 분기합니다. arm64의 MTE 태그 읽기·쓰기는 전용 처리기로 보내고 나머지는 공통 처리기로 넘깁니다.
case PTRACE_PEEKMTETAGS:arm64 MTE tag처럼 공통 regset 바깥의 architecture state를 별도 request로 처리한다.
case PTRACE_POKEMTETAGS:메모리 할당 태그를 쓰는 PTRACE_POKEMTETAGS 요청입니다. 바로 앞 읽기 요청과 같은 처리기를 사용하되 request 값으로 방향을 구분합니다.
return mte_ptrace_copy_tags(child, request, addr, data);추적 대상의 지정 주소에 있는 MTE 할당 태그를 읽거나 씁니다. request가 방향을 결정하며, 사용자 전달 자료와 복사 결과는 MTE 전용 처리기가 검사합니다.
return ptrace_request(child, request, addr, data);architecture 전용 request가 아닌 항목은 공통 ptrace core로 위임한다.
enum ptrace_syscall_dir {시스템 콜 추적 정지가 진입인지 종료인지 표현할 열거형입니다. 그 값을 ptrace ABI가 지정한 레지스터에 잠시 넣습니다.
PTRACE_SYSCALL_ENTER = 0,시스템 콜을 실행하기 전의 추적 정지를 숫자 0으로 나타냅니다.
PTRACE_SYSCALL_EXIT,바로 앞 0 다음 값인 1로 시스템 콜 종료 정지를 나타냅니다. 추적기는 이 값으로 진입 정지와 구분합니다.
static void report_syscall(struct pt_regs *regs, enum ptrace_syscall_dir dir)ptrace에 시스템 콜의 진입 또는 종료를 보고합니다. regs의 특정 레지스터를 임시 표지로 사용한 뒤 원래 값으로 되돌리는 ABI 처리를 포함합니다.
int regno;진입·종료 표지를 넣을 레지스터 번호입니다. AArch32는 r12, AArch64는 x7을 선택합니다.
unsigned long saved_reg;표지를 덮어쓰기 전 레지스터 값을 보관합니다. 추적 정지 중 이 레지스터를 바꿔도 보고 뒤에는 이 원래 값을 복원합니다.
regno = (is_compat_task() ? 12 : 7);ptrace에 시스템 콜 진입·종료 방향을 잠시 표시할 레지스터를 고릅니다. 32비트 호환 태스크는 r12, AArch64 태스크는 x7을 쓰며, 원래 값은 저장했다가 보고 후 되돌립니다.
saved_reg = regs->regs[regno];r12 또는 x7의 원래 값을 saved_reg에 보관합니다. 다음 줄의 진입·종료 표지가 실제 사용자 값을 영구히 바꾸지 않게 합니다.
regs->regs[regno] = dir;선택한 레지스터에 진입 0 또는 종료 1을 넣어 추적기가 정지 종류를 알아보게 합니다. 보고가 끝난 뒤 이 값은 원래 값으로 복구됩니다.
if (dir == PTRACE_SYSCALL_ENTER) {시스템 콜 진입 정지인지 구분합니다. 진입 경로에서는 추적기 판단에 따라 실제 시스템 콜 실행을 생략할 수도 있습니다.
if (ptrace_report_syscall_entry(regs))진입 정지를 보고한 결과 시스템 콜을 실행하지 않아야 하는지 확인합니다. 참이면 다음 줄이 syscallno를 NO_SYSCALL로 바꿉니다.
forget_syscall(regs);추적기가 이 시스템 콜의 실행을 취소하도록 요청한 경우 regs->syscallno를 NO_SYSCALL로 바꿉니다. 이후 디스패치가 원래 번호의 시스템 콜을 실행하지 않도록 남기는 표시입니다.
regs->regs[regno] = saved_reg;진입 보고가 끝났으므로 임시 방향 표지 대신 원래 사용자 레지스터 값을 되돌립니다.
} else if (!test_thread_flag(TIF_SINGLESTEP)) {종료 보고이면서 단일 단계 추적이 아닌지 검사합니다. 일반 종료 정지는 임시 표지를 유지한 채 보고하고, 단일 단계는 별도 경로로 처리합니다.
ptrace_report_syscall_exit(regs, 0);시스템 콜 종료 지점을 ptrace 추적기에 보고합니다. 두 번째 인자 0은 단일 단계 실행에 따른 가상 종료 보고가 아닌 일반 시스템 콜 종료 정지임을 나타냅니다.
regs->regs[regno] = saved_reg;일반 종료 정지를 추적기에 보고한 뒤 레지스터를 원래 값으로 복원합니다.
} else {종료 시 단일 단계 실행 중인 경로입니다. pseudo-step 보고는 일반 시스템 콜 정지와 달리 레지스터를 덮은 상태로 보여 주지 않습니다.
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개 구문에 설명을 붙였습니다.
#if defined CONFIG_X86_32 || defined CONFIG_IA32_EMULATION32비트 커널이거나 64비트 커널에서 IA32 호환 실행을 지원하면 32비트 레지스터 보기 형식을 선언합니다. ptrace가 32비트 프로그램의 레지스터 폭과 배치를 정확히 다루기 위한 정의입니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
static const struct user_regset_view user_x86_32_view; /* Initialized below. */32비트 x86 레지스터의 배치와 읽기·쓰기 함수를 묶은 regset 보기의 선언입니다. 실제 항목 초기화는 파일 아래에 있습니다.
#endif723행에서 시작한 선택 구간을 마칩니다. 32비트 커널이거나 64비트 커널에서 IA32 호환 실행을 지원하면 32비트 레지스터 보기 형식을 선언합니다. ptrace가 32비트 프로그램의 레지스터 폭과 배치를 정확히 다루기 위한 정의입니다.
#ifdef CONFIG_X86_6464비트 x86 커널용 레지스터 보기 형식을 선언합니다. 32비트 사용자 레지스터 집합과 크기·구성이 달라 별도의 user_regset_view를 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
static const struct user_regset_view user_x86_64_view; /* Initialized below. */64비트 x86용 regset 보기의 선언입니다. 아래 arch_ptrace가 native 64비트 요청을 처리할 때 이 구성을 고릅니다.
#endif726행에서 시작한 선택 구간을 마칩니다. 64비트 x86 커널용 레지스터 보기 형식을 선언합니다. 32비트 사용자 레지스터 집합과 크기·구성이 달라 별도의 user_regset_view를 사용합니다.
long arch_ptrace(struct task_struct *child, long request,x86의 ptrace 요청을 처리합니다. 추적 대상 child의 일반·디버그 레지스터 등 요청별 자료를 읽거나 바꿉니다.
unsigned long addr, unsigned long data)addr는 USER 영역 오프셋 등 요청 대상 위치이고 data는 쓸 값 또는 결과 버퍼 주소입니다. 어느 의미인지는 request가 결정합니다.
int ret;ptrace 요청의 성공·오류 결과를 저장할 ret를 선언합니다. 뒤의 요청별 분기에서 값을 정한 후 호출자에게 전달하며 이 줄은 추적 대상의 CPU 상태를 복원하지 않습니다.
unsigned long __user *datap = (unsigned long __user *)data;data를 사용자 공간의 워드 포인터로 해석합니다. PEEKUSR 같은 읽기 요청의 결과를 추적기 메모리에 써 줄 때 사용합니다.
#ifdef CONFIG_X86_64네이티브 ptrace 요청의 레지스터 보기 형식을 커널 비트수에 맞춥니다. 64비트 빌드는 user_x86_64_view를 선택하고 32비트 빌드는 아래 분기의 user_x86_32_view를 사용합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
const struct user_regset_view *regset_view = &user_x86_64_view;64비트 커널의 native ptrace 요청이므로 64비트 레지스터 배치를 선택합니다. 이어지는 GETREGS 등의 전체 레지스터 복사에서 사용합니다.
#else네이티브 32비트 ptrace 요청이므로 user_x86_32_view를 사용합니다. 디버거에 보일 레지스터의 크기와 배치를 32비트 사용자 ABI에 맞춥니다.
const struct user_regset_view *regset_view = &user_x86_32_view;32비트 커널의 native 요청에는 32비트 레지스터 배치를 선택합니다. 64비트 커널의 별도 호환 처리 경로와는 다른 전처리 분기입니다.
#endif736행에서 시작한 선택 구간을 마칩니다. 네이티브 ptrace 요청의 레지스터 보기 형식을 커널 비트수에 맞춥니다. 64비트 빌드는 user_x86_64_view를 선택하고 32비트 빌드는 아래 분기의 user_x86_32_view를 사용합니다.
switch (request) {request 값으로 USER 영역 한 워드 접근, 전체 레지스터 접근 등의 ptrace 작업을 구분합니다.
case PTRACE_PEEKUSR: {전통적인 USER 영역에서 한 워드를 읽는 요청입니다. 실제 위치에 따라 일반 레지스터 또는 디버그 레지스터 저장 상태에서 값을 가져옵니다.
unsigned long tmp;추적기에 돌려줄 한 워드 값을 임시 보관합니다. 유효한 레지스터 위치이면 읽은 값으로, 그 외 허용된 USER 빈 영역이면 0으로 채웁니다.
ret = -EIO;USER 영역 읽기의 기본 결과를 -EIO로 둡니다. addr가 워드 경계에 맞지 않거나 struct user 범위 밖이면 다음 검사에서 빠져나가 이 오류를 반환합니다.
if ((addr & (sizeof(data) - 1)) || addr >= sizeof(struct user))오프셋이 워드 크기로 정렬되지 않았거나 struct user 밖이면 접근을 거절합니다. 올바른 레지스터 필드 경계로만 읽도록 제한합니다.
break;잘못된 USER 오프셋이므로 switch를 빠져나갑니다. 위에서 준비한 -EIO가 결과로 유지됩니다.
tmp = 0; /* Default return condition */유효 범위이지만 일반·디버그 레지스터에 해당하지 않는 USER 영역은 0을 읽은 것으로 처리하도록 기본값을 둡니다.
if (addr < sizeof(struct user_regs_struct))USER 영역 앞쪽의 일반 레지스터 구조체 안인지 확인합니다. 해당하면 getreg로 대상 태스크의 저장 레지스터를 읽습니다.
tmp = getreg(child, addr);PTRACE_PEEKUSR가 요청한 user 영역 오프셋 addr에 대응하는 자식 태스크의 레지스터 값을 읽어 tmp에 둡니다. 실제 CPU의 현재 레지스터가 아니라 추적 대상에 저장된 레지스터 상태를 조회합니다.
else if (addr >= offsetof(struct user, u_debugreg[0]) &&일반 레지스터가 아니라면 디버그 레지스터 배열의 시작 이상인지 확인합니다. 다음 조건과 함께 DR0부터 DR7 사이만 허용합니다.
addr <= offsetof(struct user, u_debugreg[7])) {오프셋이 마지막 디버그 레지스터 DR7을 넘지 않는지도 검사합니다. 두 범위 조건을 만족할 때만 디버그 레지스터 번호로 바꿔 읽습니다.
addr -= offsetof(struct user, u_debugreg[0]);addr에서 struct user 안의 u_debugreg 배열 시작 오프셋을 뺍니다. 구조체 전체 기준 바이트 위치를 디버그 레지스터 배열 내부의 위치로 바꾸는 계산이며, offsetof는 실행 함수가 아닙니다.
tmp = ptrace_get_debugreg(child, addr / sizeof(data));배열 내부 바이트 오프셋을 한 항목의 크기로 나누어 DR 번호를 구하고, 해당 디버그 레지스터의 저장 상태를 tmp로 읽습니다. 결과는 아래 put_user를 통해 추적기에 전달됩니다.
ret = put_user(tmp, datap);추적 대상에서 얻은 tmp 값을 추적자 프로세스의 사용자 주소 datap에 기록합니다. 복사가 성공하면 ret는 0, 잘못된 사용자 주소로 실패하면 -EFAULT가 됩니다.
break;읽은 워드의 사용자 버퍼 복사를 마치고 요청 분기를 종료합니다. put_user가 정한 성공 또는 접근 오류가 ret에 남습니다.
case PTRACE_POKEUSR: /* write the word at location addr in the USER area */USER 영역의 한 워드를 새 값 data로 바꾸는 요청입니다. 아래에서 일반 레지스터와 디버그 레지스터 쓰기를 구분합니다.
ret = -EIO;USER 영역 쓰기의 기본 결과를 -EIO로 설정합니다. 뒤의 정렬·범위 검사와 지원 레지스터 선택을 통과한 경우에만 실제 쓰기 결과로 바뀝니다.
if ((addr & (sizeof(data) - 1)) || addr >= sizeof(struct user))쓰기 위치가 워드 정렬을 지키고 struct user 범위 안에 있는지 검사합니다. 잘못된 위치에 레지스터 값을 쓰지 않게 합니다.
break;잘못된 쓰기 오프셋이므로 레지스터를 수정하지 않고 분기를 끝냅니다. 결과는 기본 오류 -EIO입니다.
if (addr < sizeof(struct user_regs_struct))일반 레지스터 구조체의 위치이면 putreg로 해당 레지스터를 갱신합니다. 세그먼트나 플래그 등 필드별 제한은 그 처리기가 검사합니다.
ret = putreg(child, addr, data);USER 영역의 일반 레지스터 오프셋에 data를 써 추적 대상의 저장 레지스터 상태를 갱신합니다. putreg가 세그먼트·flags·FS/GS base 등의 제약도 처리하며 성공 여부를 ret에 돌려줍니다.
else if (addr >= offsetof(struct user, u_debugreg[0]) &&일반 레지스터 밖의 요청이 디버그 레지스터 배열 시작 이상인지 검사합니다.
addr <= offsetof(struct user, u_debugreg[7])) {DR7 위치 이하인 요청만 디버그 레지스터 쓰기로 처리합니다. 허용 범위를 벗어나면 ret의 기본 오류값이 유지됩니다.
addr -= offsetof(struct user, u_debugreg[0]);쓰기 요청의 addr를 u_debugreg 배열 내부 바이트 오프셋으로 바꿉니다. 이어서 sizeof(data)로 나눈 값이 변경할 디버그 레지스터 번호가 됩니다.
ret = ptrace_set_debugreg(child,추적 대상의 디버그 레지스터 설정 함수를 호출하는 첫 줄입니다. 다음 줄의 addr/sizeof(data)가 DR 번호, data가 새 값이며 breakpoint 주소나 DR6·DR7 상태를 검사·갱신한 결과를 ret에 받습니다.
addr / sizeof(data), data);앞줄의 ptrace_set_debugreg 호출에 디버그 레지스터 번호와 새 값 data를 전달하는 부분입니다. 중단점 관련 제약을 검사하며 추적 대상 상태를 갱신하고, 성공 또는 오류 코드는 ret로 받습니다.
break;일반 또는 디버그 레지스터 쓰기를 마치고 분기를 끝냅니다. 실제 쓰기 처리기가 돌려준 성공·오류가 최종 결과가 됩니다.
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개 구문에 설명을 붙였습니다.
else요청한 n번째 워드가 저장된 SP와 같은 커널 스택 범위를 벗어난 경우입니다. 임의 커널 주소를 역참조하지 않고 실패값을 돌려줍니다.
return 0;요청 위치가 해당 커널 스택 밖이므로 0을 반환합니다. 주소 0에서 읽는 동작이 아니라 범위 밖 접근을 거절한 결과입니다.
void ptrace_disable(struct task_struct *child)추적 해제 시 아키텍처별 정리를 수행할 자리입니다. 이 버전 RISC-V 구현은 빈 함수이므로 child의 레지스터나 추적 상태를 추가 변경하지 않습니다.
long arch_ptrace(struct task_struct *child, long request,RISC-V의 ptrace 진입점입니다. 이 함수에는 독자적인 요청 case가 없어 실제 요청 처리는 공통 ptrace_request가 맡습니다.
unsigned long addr, unsigned long data)추적 대상에 적용할 주소와 자료 값을 받습니다. 이 함수에서 별도 변환 없이 요청 번호와 함께 공통 처리기로 전달합니다.
long ret = -EIO;ptrace 처리 결과를 담을 long 변수를 -EIO로 초기화합니다. 이 함수의 현재 switch는 모든 요청을 아래의 공통 처리기로 넘기므로 실제 결과로 다시 대입됩니다.
switch (request) {요청 번호를 구분할 switch이지만 현재는 default만 있습니다. 모든 요청이 아래의 공통 ptrace 처리로 이어집니다.
default:모든 요청에 적용되는 기본 분기입니다. 공통 ptrace가 지원 여부와 필요한 자료 접근을 판단합니다.
ret = ptrace_request(child, request, addr, data);추적 대상 child와 요청 종류·주소·데이터를 공통 ptrace 처리기에 넘깁니다. RISC-V 전용 분기가 없는 요청의 실제 처리 결과를 ret에 저장합니다.
break;공통 처리기가 반환한 결과를 ret에 받은 뒤 switch를 나갑니다. 함수 아래에서 그 결과를 그대로 반환합니다.
return ret;공통 ptrace 처리기가 반환한 결과를 그대로 돌려줍니다. 반환값은 요청의 성공·오류 상태이며 privilege나 인터럽트 마스크를 복원하는 명령이 아닙니다.
#ifdef CONFIG_COMPATCOMPAT은 64비트 커널에서 32비트 사용자 ABI를 지원하는 구성입니다. 켜지면 64비트 내부 pt_regs를 호환 레지스터 구조체로 바꿔 읽고 쓰는 ptrace helper를 포함하며, 꺼지면 그 별도 변환 경로를 생략합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
static int compat_riscv_gpr_get(struct task_struct *target,호환 태스크의 일반 레지스터를 읽어 32비트 ABI 형식으로 내보내는 regset 콜백입니다. target은 레지스터를 읽을 태스크입니다.
const struct user_regset *regset,어떤 regset 항목을 요청했는지 설명하는 메타데이터를 받습니다. 이 콜백의 본문은 고정된 호환 일반 레지스터 배치를 사용합니다.
struct membuf to)변환한 레지스터 자료를 쓸 membuf 출력 상태를 받습니다. 남은 버퍼 범위에 맞춘 복사는 membuf_write가 담당합니다.
struct compat_user_regs_struct cregs;커널 pt_regs를 32비트 호환 형식으로 변환해 담을 임시 구조체입니다. 변환한 자료를 그대로 출력 버퍼로 보냅니다.
regs_to_cregs(&cregs, task_pt_regs(target));추적 대상의 커널 pt_regs를 32비트 호환 ABI가 기대하는 compat_user_regs_struct로 변환합니다. 커널 내부 레지스터 표현을 그대로 내보내지 않고 호환 프로세스의 자료 폭과 배치에 맞춥니다.
return membuf_write(&to, &cregs,변환한 cregs를 출력 버퍼에 쓰고 membuf_write의 결과를 반환합니다. 뒤의 크기 인자는 호환 레지스터 구조체 전체 길이입니다.
sizeof(struct compat_user_regs_struct));앞줄의 membuf_write에 복사할 32비트 호환 레지스터 구조체의 전체 크기를 넘깁니다. 변환된 cregs를 regset 출력 버퍼에 쓰는 작업의 길이이며, 별도의 함수 호출은 아닙니다.
static int compat_riscv_gpr_set(struct task_struct *target,호환 ABI로 받은 일반 레지스터 값을 대상 태스크에 반영하는 regset 쓰기 콜백입니다.
const struct user_regset *regset,호환 일반 레지스터 항목의 메타데이터를 받습니다. 실제 변환에는 compat_user_regs_struct 배치를 사용합니다.
unsigned int pos, unsigned int count,입력 자료의 시작 바이트 위치 pos와 남은 바이트 수 count를 받습니다. user_regset_copyin이 이 범위에 맞춰 입력을 가져옵니다.
const void *kbuf, const void __user *ubuf)입력이 커널 버퍼에 있으면 kbuf, 사용자 버퍼에 있으면 ubuf로 받습니다. 공통 regset 복사기가 적절한 쪽에서 읽습니다.
int ret;호환 모드 레지스터 자료를 복사한 결과를 저장할 int 변수를 선언합니다. 대상 레지스터 집합은 다음의 cregs에 준비합니다.
struct compat_user_regs_struct cregs;입력받은 32비트 레지스터 묶음을 임시 보관할 구조체입니다. 이후 cregs_to_regs가 대상 pt_regs의 내부 형식으로 변환합니다.
ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &cregs, 0, -1);호출자가 지정한 pos·count 구간의 자료를 커널 버퍼 kbuf 또는 사용자 버퍼 ubuf에서 호환 모드 레지스터 구조체 cregs로 복사합니다. 복사 위치와 남은 길이를 갱신하고 성공·오류 결과를 ret에 받습니다.
cregs_to_regs(&cregs, task_pt_regs(target));입력받은 호환 레지스터 구조체 cregs를 추적 대상의 pt_regs 표현으로 되돌려 반영합니다. 바로 앞의 복사 결과는 ret로 따로 반환하며, 이 줄 앞에 성공 여부를 검사하는 분기가 있는 것은 아닙니다.
return ret;호환 모드 레지스터 입력의 복사 결과를 반환합니다. 앞에서 cregs_to_regs로 대상 pt_regs에 값을 옮긴 뒤의 C 함수 반환이며, 이 줄에서 대상 태스크를 실행시키지는 않습니다.
static const struct user_regset compat_riscv_user_regset[] = {호환 프로세스가 공개하는 regset 항목 배열입니다. 각 항목이 자료 크기·정렬과 읽기·쓰기 콜백을 연결하여 ptrace와 core dump에 같은 배치를 제공합니다.
05 · WORKED EXAMPLE
arm64 watchpoint 설정 경로 검산
주소 0x7fff2000을 감시하고 싶다면
주소는 예시다. 요청한 길이·접근 형식과 slot 수는 실행 중인 커널과 CPU에서 검증해야 하며, 여기서는 CPU0에서 CPU3으로 옮겨 갈 때의 레지스터 복원을 가정하지 않는다.
- 지원 확인
NT_ARM_HW_WATCHregset의 resource info는hw_breakpoint_slots(TYPE_DATA)로 얻은 slot 수를 포함한다 (slot 수). - 주소·제어 입력
hw_break_set()은 regset의 address/control 쌍을 순서대로 읽어ptrace_hbp_set_addr()/ptrace_hbp_set_ctrl()로 전달한다 (regset 쓰기). - task 상태필요하면
register_user_hw_breakpoint(..., tsk)로 perf event를 만들고tsk->thread.debug.hbp_watch[]에 둔다. 제어값은arch_bp_generic_fields()와modify_user_hw_breakpoint()경로에서 검증·변경한다 (event 생성·필드 변환, 제어값 검증·반영). - 적중·해제적중 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
소스에 나타난 동작과 별도 검증할 동작
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·호환 변환은 서로 다른 근거다.
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의 구체적 시점은 이 발췌에서 추론하지 않는다.
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를 만든다고 단정하지 않는다.
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 삽입 코드가 없다. 그런 동작은 디버거 구현과 해당 커널의 별도 지원 경로를 확인한 뒤에만 설명할 수 있다.
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 경로를 따로 추적해야 한다.
지연은 요청별로 계측한다
같은 대상과 workload에서 ptrace stop/resume, regset 전송 byte 수, breakpoint callback·SIGTRAP 빈도를 따로 기록한다. vector 상태 저장이 필요한 경우와 그렇지 않은 경우도 분리한다 (RISC-V vector get).
평균뿐 아니라 각 단계의 최대값과 CPU 이동 여부를 기록한다. 펌웨어 호출 시간·재시도 횟수는 이 ptrace 소스에 나타나지 않으므로, 별도 펌웨어 경로가 실제 workload에 있음을 입증하지 않았다면 여기의 지연 항목으로 넣지 않는다.
07 · FAILURE
오류를 어떤 경로에서 확인할 것인가
| 분류 | 관찰되는 결과·경계 | 첫 확인값 |
|---|---|---|
| arm64 | HW_BREAK/HW_WATCH regset은 빌드 조건부다. control 변환·event 수정에서 오류가 반환될 수 있다. 아래 MTE 요청 분기는 hardware watchpoint 검증 자체를 보여 주지 않는다. | 커널 구성, regset resource info의 slot 수, address/control, ptrace_hbp_fill_attr_ctrl() 반환값 (검증·slot 수). |
| x86-64 | USER 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
지원 여부부터 확인하는 계측
- arm64는 실행 커널의
CONFIG_HAVE_HW_BREAKPOINT와NT_ARM_HW_WATCHresource info를, x86은 DR USER offset 요청의 반환값을 먼저 기록한다. GDB watchpoint와 task affinity 변경 실험은 실제 slot·권한·디버거 지원이 확인된 환경에서만 한다. - RISC-V hardware watchpoint나 single-step 실험은 해당 커널과 디버거가 그 경로를 제공한다는 별도 근거를 확보한 경우에만 진행한다. 이 글의
arch_ptrace()발췌만으로 지원을 가정하지 않는다. - FP/vector regset은 커널 구성과 대상 상태를 기록한 뒤 읽기 전후 시간·전송 byte 수·반환값을 측정한다. RISC-V vector는 비활성 상태의
-EINVAL과vlenb불일치를 따로 기록한다. - 동일 workload에서 ptrace 요청 종류, stop/resume 시각, CPU 번호, PC와 오류 코드를 같은 표에 기록한다. CPU별 debug register 설치 여부는 별도 hw_breakpoint/perf 추적 결과로 판정한다.
09 · REFERENCES
원문 위치
- arm64 표시 코드arch/arm64/kernel/ptrace.c:2296–2354
- arm64 별도 hardware debug 근거ptrace.c:167–552 · ptrace.c:1618–1635
- x86-64 표시 코드arch/x86/kernel/ptrace.c:722–780
- x86-64 DR7 helperptrace.c:560–687
- RISC-V 표시 코드arch/riscv/kernel/ptrace.c:331–381
- RISC-V 별도 regset 근거ptrace.c:23–216
Linux kernel source: GPL-2.0-only. 표시된 코드와 별도 참고 코드 모두 Linux v6.18.37을 가리킵니다. debugger 구현·CPU debug register 설치와 같은 별도 경로는 이 글의 발췌만으로 결론 내리지 않습니다.