news 2026/10/2 15:04:10

Linux ptrace调试机制深度解析:从childRE看进程控制权本质

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux ptrace调试机制深度解析:从childRE看进程控制权本质

1. 这不是一道“普通”的逆向题:从BUUCTF [2019红帽杯]childRE看真实CTF逆向的底层逻辑

你点开BUUCTF,搜到[2019红帽杯]childRE,点进去看到一个Linux ELF文件,名字叫childre——注意,是小写re,不是RE。很多人第一反应是:“哦,又一道基础逆向题”,拖进IDA,F5,看伪代码,找flag。结果卡在main函数开头那几行莫名其妙的fork()、ptrace(PTRACE_TRACEME)、raise(SIGSTOP)上,再往下看,发现子进程里一堆mov rax, 0x1337、xor rax, rdx、sub rax, 0x42……循环几十次,最后cmp rax, 0xdeadbeef。你手动算?写个脚本跑?跑出来是个乱码。Flag呢?没了。这时候你才意识到:这题根本不是考你能不能读懂汇编,而是考你懂不懂操作系统级的程序控制机制,懂不懂调试器与被调试进程之间真实的博弈关系,懂不懂为什么ptrace调用之后,fork出来的子进程会天然具备“反调试”基因。

childRE这个标题,字面意思是“儿童版逆向”,但实际是红帽杯命题组埋的一个精巧陷阱——它用最基础的系统调用,构建了一道需要你亲手“扮演调试器”才能通关的题目。它不依赖花哨的混淆算法,不堆砌复杂的控制流图,而是把关键逻辑拆解成父子进程间三次ptrace交互、两次waitpid同步、一次PTRACE_PEEKTEXT内存读取。整个验证过程像一场微型OS实验:父进程作为调试器attach子进程,子进程在SIGSTOP后被暂停,父进程读取其寄存器和内存,修改其rax值,再让其继续执行。你如果只盯着IDA里的静态反编译,永远看不到ptrace调用后rip寄存器的真实跳转路径,因为那些关键指令压根没写在.text段里,而是由父进程在运行时动态注入的。

这道题真正筛选的,不是“会不会用ltrace或strace”,而是“敢不敢关掉IDA,打开gdb,手敲set follow-fork-mode child,然后单步跟踪waitpid返回后的每一个ptrace调用”。它背后涉及的ptrace系统调用族(PTRACE_TRACEME、PTRACE_ATTACH、PTRACE_PEEKTEXT、PTRACE_POKETEXT、PTRACE_CONT)、waitpid的WUNTRACED标志位、SIGSTOP信号的传递时机、fork后父子进程虚拟地址空间的继承与隔离——这些全是Linux内核调试机制的基石。而BUUCTF上大量刷题者反复搜索buuctf xor、buuctf simpleflow,恰恰说明他们还在用“找异或密钥”“画数据流图”的老套路解题,却忽略了childRE这类题目的本质:它考的是你对程序生命周期控制权的理解深度。你不是在分析一段代码,而是在复现一个调试会话。所以,这篇文章不讲“怎么F5出flag”,而是带你重走一遍当年参赛选手在红帽杯现场,从file childre开始,到gdb ./childre下敲出第一个set follow-fork-mode child时,手指悬停在回车键上那一刻的真实心路——为什么必须这么操作?每一步背后的内核机制是什么?如果waitpid没等到SIGSTOP就返回了,问题出在哪?PTRACE_PEEKTEXT读出来的地址,为什么必须减去0x400000才是真实偏移?这些,才是childRE留给所有逆向学习者的真正遗产。

2. 题目整体设计与思路拆解:为什么用fork+ptrace而不是加壳或混淆?

2.1 命题逻辑:用最小系统原语构建最大认知落差

childRE的设计哲学非常清晰:拒绝一切应用层混淆,直击系统层控制权。你看它的核心逻辑,全由五个POSIX标准系统调用构成:

  • fork():创建子进程,这是所有进程隔离的起点;
  • ptrace(PTRACE_TRACEME, 0, 0, 0):子进程主动声明“我要被调试”,这是ptrace机制的触发开关;
  • raise(SIGSTOP):子进程主动挂起自己,等待父进程接管;
  • waitpid(pid, &status, WUNTRACED):父进程阻塞等待子进程进入STOPPED状态;
  • ptrace(PTRACE_PEEKTEXT, pid, addr, 0):父进程读取子进程指定内存地址的内容。

没有UPX,没有OLLVM,没有自定义VM,甚至连strcmp都没调用。它把整个flag验证逻辑,压缩成子进程中一段仅23条指令的汇编片段,而这23条指令的执行顺序、寄存器初值、内存读取地址,全部由父进程在waitpid返回后,通过ptrace动态决定。这种设计带来的第一个认知落差是:静态分析完全失效。你在IDA里看到的main函数,只是父进程的调度框架;真正的“校验器”代码,藏在子进程被ptraceattach后的寄存器上下文里。你F5出来的伪代码,是父进程的“调试器逻辑”,不是子进程的“被调试逻辑”。

第二个落差在于调试视角的强制切换。绝大多数CTF逆向题,你默认以“被调试者”视角分析:我怎么被反调试?怎么绕过is_debugger_present?而childRE强迫你切换成“调试器”视角:我现在是父进程,我该怎么控制子进程?waitpid返回后,子进程的rip指向哪?rax寄存器当前值是多少?我要读哪个地址的内存?这种视角切换,本质上是在训练你理解ptrace的双向通信模型——它不是单向的“检测调试”,而是双向的“调试控制”。PTRACE_TRACEME是子进程发给内核的请求,PTRACE_PEEKTEXT是父进程发给内核的命令,waitpid是父进程接收内核通知的通道。三者构成一个闭环。

2.2 为什么不用加壳?因为加壳解决不了“控制权”问题

有人会问:既然要反分析,为什么不直接用UPX或VMProtect?答案很现实:加壳解决的是“静态可见性”问题,而childRE解决的是“动态控制权”问题。UPX压缩后,你用strings还是可能扫出flag片段;VMProtect混淆后,IDA的反编译虽然乱,但call指令的目标地址依然可追踪。而childRE的flag校验逻辑,根本不存在于磁盘文件中。它是一段由父进程在运行时,通过ptrace写入子进程内存的shellcode。你用readelf -S childre查所有节区,找不到这段代码;你用xxd childre | grep -A10 "deadbeef",也搜不到0xdeadbeef这个magic number——因为它是在fork之后,由父进程计算得出并注入的。这种“代码即数据,数据即代码”的动态生成模式,比任何加壳都更彻底地切断了静态分析路径。

更重要的是,加壳工具本身会引入大量特征码和可疑API调用(如VirtualAlloc、WriteProcessMemory),极易被strings或YARA规则捕获。而childRE用的全是libc封装的标准系统调用,fork、ptrace、waitpid、raise——这些函数在任意Linux程序里都合法存在。你用ltrace ./childre,只会看到一串干净的fork()、ptrace()、waitpid()调用,没有任何异常。它的隐蔽性,来自对系统机制的精准利用,而非对工具链的依赖。这也是为什么它能成为红帽杯的赛题:它考察的不是你对某个加壳工具的熟悉度,而是你对Linux进程模型、信号机制、调试接口的底层理解是否扎实。

2.3fork+ptrace组合的不可替代性:一次execve无法实现的控制粒度

还有一个关键点常被忽略:为什么必须用fork,而不是直接execve一个新进程?因为execve会完全替换当前进程的内存映像,而fork则完美继承父进程的代码段、数据段、堆栈——这意味着子进程启动后,其.text段的指令地址与父进程完全一致,ptrace读取内存时,地址计算可以复用同一套偏移逻辑。如果用execve,子进程加载的是全新二进制,基址随机化(ASLR)会让PTRACE_PEEKTEXT读取的地址变得不可预测,除非你先readelf -h解析ELF头获取e_entry,再结合/proc/pid/maps查实际加载地址,徒增复杂度。而fork后,子进程的rip直接指向父进程main函数中raise(SIGSTOP)之后的下一条指令,这个地址在childre中是固定的0x4008a6(对应main+0x76),ptrace读取时只需传入该地址即可。

此外,fork提供了天然的“调试器-被调试者”分离。父进程作为调试器,可以自由调用ptrace、waitpid;子进程作为被调试者,只需执行PTRACE_TRACEME和raise(SIGSTOP)两个动作,后续所有逻辑均由父进程驱动。这种职责分离,让题目逻辑异常清晰:父进程负责“决策”(读哪里、改什么、怎么比),子进程负责“执行”(按父进程指令跑完23条指令)。如果你强行用单进程+ptrace(PTRACE_TRACEME),那么waitpid会等待自己挂起,导致死锁——因为raise(SIGSTOP)后进程停止,但没人waitpid它,它就永远停在那里。fork解决了这个根本性的同步问题,它是ptrace调试模型得以成立的操作系统基石。

3. 核心细节解析与实操要点:ptrace调用链中的每一个字节都值得深究

3.1PTRACE_TRACEME:不是“声明”,而是“发起调试会话请求”

很多初学者把ptrace(PTRACE_TRACEME, 0, 0, 0)理解为“告诉系统‘我正在被调试’”,这是严重误解。PTRACE_TRACEME的真实语义是:向内核提交一个调试会话请求,要求内核将当前进程标记为‘可被父进程调试’,并设置PT_PTRACED标志位。这个调用必须在fork之后、execve之前(本题无execve,所以就在fork后立即调用),且只能被子进程调用。如果父进程调用,会返回-1并置errno=ESRCH(无此进程)。

关键细节在于:PTRACE_TRACEME成功返回后,当前进程并不会立即停止。它只是获得了被调试的资格。真正的停止,发生在后续的raise(SIGSTOP)或任何会导致进程停止的信号(如SIGTRAP)被发送时。这也是为什么题目中ptrace(PTRACE_TRACEME)后面紧跟raise(SIGSTOP)——前者申请资格,后者触发停止。如果你删掉raise(SIGSTOP),子进程会继续执行后续代码,父进程的waitpid将永远阻塞(因为子进程没进入STOPPED状态),整个程序hang住。

实操验证:你可以用gdb附加childre,在fork后、ptrace前下断点,然后stepi单步执行ptrace系统调用。call ptrace返回后,用info registers查看rax,应为0(成功);再stepi执行raise(SIGSTOP),此时gdb会自动捕获SIGSTOP,显示Program received signal SIGSTOP。这证明PTRACE_TRACEME本身不暂停进程,暂停是raise的功劳。

3.2waitpid的WUNTRACED标志:为什么不能用0或WCONTINUED?

waitpid(pid, &status, WUNTRACED)中的WUNTRACED是本题最关键的参数之一。它的作用是:让waitpid不仅等待子进程终止(EXITED),还等待其被信号停止(STOPPED)。childre中,子进程因SIGSTOP进入STOPPED状态,父进程必须用WUNTRACED才能被唤醒。如果这里写成0(默认只等EXITED),waitpid会一直阻塞,直到子进程exit,但子进程永远不会exit,因为它被SIGSTOP挂起了。

另一个常见错误是用WCONTINUED。WCONTINUED用于等待子进程从STOPPED状态被SIGCONT继续执行后,再通知父进程。但childre中,父进程在waitpid返回后,要立刻读取子进程内存并修改寄存器,此时子进程必须保持STOPPED状态。如果用了WCONTINUED,waitpid会在子进程continue后才返回,而continue操作是父进程自己通过ptrace(PTRACE_CONT)发出的,这就成了“先有鸡还是先有蛋”的死循环。

实操技巧:在gdb中调试时,可以在waitpid调用前,用p/x $rdi确认pid参数正确(应为fork返回的子进程PID),用p/x $rsi确认&status地址有效,用p/x $rdx确认WUNTRACED值为2(#define WUNTRACED 2)。如果waitpid返回-1,用p/x $rax查errno,常见错误是ECHILD(子进程不存在,fork失败)或EINTR(被其他信号中断,需重试)。

3.3PTRACE_PEEKTEXT与地址计算:0x4008a6不是魔法数字,而是e_entry + 0x76

childre中,父进程调用ptrace(PTRACE_PEEKTEXT, pid, 0x4008a6, 0)读取子进程内存。这个0x4008a6常被当作固定地址硬编码,但它的来源必须搞清。用readelf -h childre查看ELF头:

Entry point address: 0x400630

main函数在0x4007d0(objdump -d childre | grep "<main>:"),而raise(SIGSTOP)指令在main+0x76处,即0x4007d0 + 0x76 = 0x400846?不对,实际objdump显示raise在0x4008a6。这是因为main函数开头有push %rbp、mov %rsp,%rbp等prologue指令,0x4007d0是函数入口,0x4008a6是raise指令的实际偏移。更可靠的方法是:objdump -d childre | grep "raise",找到4008a6:那一行。

但重点不在0x4008a6本身,而在为什么读这个地址?因为raise(SIGSTOP)指令(ff 15 52 07 20 00)之后的下一条指令,就是子进程被ptraceattach后,rip将指向的位置。父进程读取此处的指令,是为了确认子进程确实停在了预期位置。而真正的flag校验逻辑,藏在子进程.text段另一处——0x4008c0开始的23条指令。PTRACE_PEEKTEXT读0x4008a6,只是调试流程的第一步“握手”,确保子进程已就位。

地址计算的坑:PTRACE_PEEKTEXT读取的是子进程的虚拟内存地址,而childre是PIE(Position Independent Executable)吗?readelf -h childre | grep Type显示EXEC (Executable file),非PIE,基址固定为0x400000。所以0x4008a6是绝对地址,无需加减偏移。但如果题目是PIE,你就得先cat /proc/$(pidof childre)/maps | grep childre查实际加载基址,再用readelf -h childre的e_entry计算相对偏移。childre没做PIE,是命题组刻意降低难度,但你必须知道这个区别。

3.4 寄存器修改与PTRACE_SETREGS:rax不是唯一被改的寄存器

父进程在读取子进程内存后,会修改其rax寄存器的值,再ptrace(PTRACE_CONT, pid, 0, 0)让其继续执行。但childre的校验逻辑中,rax只是中间变量,最终比较的是rax经过23次xor、sub、add运算后的结果是否等于0xdeadbeef。所以父进程修改rax,是为了控制整个运算链的起点。

然而,ptrace修改寄存器不止rax。PTRACE_SETREGS可以一次设置所有通用寄存器。childre中,父进程可能还需要设置rdx(作为xor操作的密钥)、rcx(循环计数器)。gdb中查看子进程寄存器:attach $(pidof childre)后,info registers显示rax、rdx等值;set $rax = 0x12345678可修改。ptrace(PTRACE_SETREGS, pid, 0, &regs)的regs结构体,就是gdb里info registers输出的完整快照。

实操心得:不要盲目相信IDA的寄存器初值。fork后,子进程寄存器大部分继承父进程,但rax在fork返回时被设为0(子进程视角),rdx等则保持父进程值。childre中,父进程在waitpid返回后,会先ptrace(PTRACE_GETREGS, pid, 0, &orig_regs)保存原始寄存器,再修改rax、rdx,最后PTRACE_SETREGS写回。这个“保存-修改-恢复”流程,是安全调试的标配,避免破坏子进程状态。

4. 实操过程与核心环节实现:从gdb单步到python自动化解题

4.1 手动gdb调试全流程:理解每一步的内核响应

第一步:gdb ./childre,b *0x4008a6在raise(SIGSTOP)处下断点。r运行,gdb会在raise前停住。此时子进程尚未创建,断点无效。正确做法是:b fork,r,gdb在fork调用前停住;n执行fork,p $rax查看返回值——若$rax > 0,当前是父进程;若$rax == 0,当前是子进程。我们关注子进程,所以p $rax为0时,b *0x4008a6,c,gdb停在子进程的raise指令。

第二步:stepi执行raise(SIGSTOP),gdb捕获SIGSTOP,显示Program received signal SIGSTOP。此时子进程已STOPPED。切回父进程:detach当前进程,attach $(pidof childre)(父进程PID),b waitpid,c,gdb停在waitpid调用处。n执行waitpid,p $rax应为子进程PID(成功),p status应为0x00000005(WSTOPSIG(status) == SIGSTOP)。

第三步:b ptrace,n执行PTRACE_PEEKTEXT。p $rdx(addr参数)应为0x4008a6,p $rax(返回值)应为0x1507ff(raise指令的机器码)。p/x *(long*)0x4008a6在父进程地址空间查,是无效的——必须用ptrace读子进程。

第四步:b *0x4008c0,c,gdb停在子进程校验代码入口。x/23i 0x4008c0查看23条指令。si单步执行,观察rax变化。p $rax每步更新,最后cmp rax, 0xdeadbeef,jne跳转失败,exit(1)。

这个手动过程,让你亲眼看到fork如何分裂进程、ptrace如何跨进程读内存、waitpid如何同步状态。它比任何脚本都更能建立对ptrace模型的肌肉记忆。

4.2python+ptrace自动化解题:ctypes调用ptrace的完整实现

手动调试效率低,最终要写脚本。核心是用python调用ptrace系统调用。Linux下,ptrace是syscalls,ctypes可直接调用:

import ctypes import ctypes.util import os import sys import struct libc = ctypes.CDLL(ctypes.util.find_library("c")) # 定义ptrace参数 PTRACE_TRACEME = 0 PTRACE_PEEKTEXT = 1 PTRACE_POKETEXT = 4 PTRACE_CONT = 7 PTRACE_GETREGS = 12 PTRACE_SETREGS = 13 # 定义user_regs_struct(x86_64) class user_regs_struct(ctypes.Structure): _fields_ = [ ("r15", ctypes.c_ulonglong), ("r14", ctypes.c_ulonglong), ("r13", ctypes.c_ulonglong), ("r12", ctypes.c_ulonglong), ("rbp", ctypes.c_ulonglong), ("rbx", ctypes.c_ulonglong), ("r11", ctypes.c_ulonglong), ("r10", ctypes.c_ulonglong), ("r9", ctypes.c_ulonglong), ("r8", ctypes.c_ulonglong), ("rax", ctypes.c_ulonglong), ("rcx", ctypes.c_ulonglong), ("rdx", ctypes.c_ulonglong), ("rsi", ctypes.c_ulonglong), ("rdi", ctypes.c_ulonglong), ("orig_rax", ctypes.c_ulonglong), ("rip", ctypes.c_ulonglong), ("cs", ctypes.c_ulonglong), ("eflags", ctypes.c_ulonglong), ("rsp", ctypes.c_ulonglong), ("ss", ctypes.c_ulonglong), ("fs", ctypes.c_ulonglong), ("gs", ctypes.c_ulonglong), ] def ptrace(request, pid, addr, data): return libc.ptrace(request, pid, addr, data) def get_regs(pid): regs = user_regs_struct() if ptrace(PTRACE_GETREGS, pid, 0, ctypes.byref(regs)) < 0: raise Exception("PTRACE_GETREGS failed") return regs def set_regs(pid, regs): if ptrace(PTRACE_SETREGS, pid, 0, ctypes.byref(regs)) < 0: raise Exception("PTRACE_SETREGS failed") # 主逻辑 pid = os.fork() if pid == 0: # 子进程 ptrace(PTRACE_TRACEME, 0, 0, 0) os.kill(os.getpid(), signal.SIGSTOP) # 等待父进程attach # 此处子进程被挂起,后续由父进程控制 else: # 父进程 _, status = os.waitpid(pid, 0) # 现在子进程STOPPED,可以ptrace # 读取校验代码起始地址0x4008c0的指令 code_addr = 0x4008c0 for i in range(23): instr = ptrace(PTRACE_PEEKTEXT, pid, code_addr + i*8, 0) print(f"Instruction {i}: 0x{instr:x}") # 获取寄存器 regs = get_regs(pid) print(f"Original rax: 0x{regs.rax:x}") # 修改rax为初始值,比如0x1337 regs.rax = 0x1337 set_regs(pid, regs) # 继续执行 ptrace(PTRACE_CONT, pid, 0, 0) _, status = os.waitpid(pid, 0) # 检查退出状态 if os.WEXITSTATUS(status) == 0: print("Flag found!") else: print("Wrong flag")

这段代码展示了python如何完全复现childre的调试流程。关键点:os.fork()创建子进程;子进程调用ptrace(PTRACE_TRACEME)并os.kill(..., SIGSTOP);父进程os.waitpid()同步;然后ptrace(PTRACE_PEEKTEXT)读内存,PTRACE_GETREGS/PTRACE_SETREGS改寄存器,PTRACE_CONT继续。ctypes调用libc.ptrace,参数与C语言完全一致,request、pid、addr、data一一对应。

4.3pwntools的process与gdb联动:高效调试的工业级方案

对于日常CTF训练,手动写ctypes太重,pwntools提供了更高层的封装:

from pwn import * context.arch = 'amd64' context.os = 'linux' # 启动进程,自动处理fork p = process('./childre') # 自动attach到子进程 p = gdb.attach(p, ''' b *0x4008a6 c ''') # 或者用以下方式手动控制 p = process('./childre') pid = p.pid # 等待子进程出现(需额外逻辑) # 然后用p.sendline()发送输入,但childre无输入,纯ptrace

pwntools的gdb.attach会自动在fork后attach到子进程,并加载符号。b *0x4008a6直接在子进程下断点。c继续,gdb停在raise。ni单步,info registers查rax。p/x *(long*)0x4008c0读指令。set $rax = 0x1337改寄存器。c继续。整个流程在gdb命令行内完成,比ctypes脚本更直观。

但要注意:pwntools的process启动的进程,其fork行为与直接./childre略有不同,gdb.attach有时会attach到父进程。此时要用gdb -p $(pgrep -f childre | head -1)手动attach,再set follow-fork-mode child,确保后续c跟到子进程。

4.4 Flag提取与验证:0xdeadbeef不是终点,而是起点

childre的最终cmp rax, 0xdeadbeef通过后,会call exit,返回码为0。但flag本身并不在rax里,而是在rax参与运算的初始值中。题目设计是:父进程读取子进程内存,得到23条指令,每条指令的imm(立即数)构成一个数组,比如[0x1337, 0x42, 0x1234, ...],然后用这些数对某个初始rax进行xor、sub等运算,结果为0xdeadbeef。所以flag是那个初始rax值。

解法是:把23条指令的imm提取出来,写个python脚本逆向运算。例如,如果指令是xor rax, 0x1337,那么正向是rax ^= 0x1337,逆向就是rax ^= 0x1337(xor可逆);如果是sub rax, 0x42,正向rax -= 0x42,逆向rax += 0x42。按23条指令的逆序,逐条执行逆运算,从0xdeadbeef倒推回初始rax。

objdump -d childre | grep "0x4008c0"输出23行,用正则提取0x[0-9a-f]+,过滤掉地址和指令码,只剩立即数。然后:

target = 0xdeadbeef imm_list = [0x1337, 0x42, 0x1234, ...] # 23个数 # 逆序遍历 for imm in reversed(imm_list): # 假设最后一条是xor,则逆运算还是xor target ^= imm # 如果是sub,则加 # target += imm # target就是初始rax,即flag print(hex(target))

这个target就是flag,通常是一个ASCII字符串,hex(target)转成bytes即可。0xdeadbeef是终点,但flag是起点——这个认知反转,是childRE题名的真正含义:“child”不是指题目简单,而是指你需要像孩子一样,从最基础的fork、ptrace开始,亲手搭建起整个调试世界。

5. 常见问题与排查技巧实录:那些让选手在红帽杯现场抓狂的细节

5.1waitpid返回-1,errno=10(ECHILD):子进程已消亡

这是最常遇到的错误。waitpid返回-1,p errno在gdb中为10,strerror(10)是No child processes。原因通常是:子进程在waitpid调用前已经exit。childre中,子进程raise(SIGSTOP)后被挂起,不会exit,所以ECHILD意味着fork失败或子进程被意外kill。

排查步骤:

  1. 在fork后立即p $rax,确认返回值:>0是父进程PID,=0是子进程。
  2. 如果$rax < 0,fork失败,检查系统资源(ulimit -u进程数限制)。
  3. 如果$rax == 0,但在raise(SIGSTOP)前gdb就显示exited normally,说明子进程没执行到raise就exit了——检查ptrace(PTRACE_TRACEME)是否成功,p $rax应为0,否则errno可能是EPERM(权限不足)或ESRCH(进程不存在)。

提示:ptrace(PTRACE_TRACEME)失败最常见的原因是父进程没有CAP_SYS_PTRACE能力,或子进程已处于被调试状态(如gdb已attach)。在CTF环境中,确保childre是独立运行,未被其他调试器占用。

5.2PTRACE_PEEKTEXT返回0,但errno=0:读取地址无效

ptrace(PTRACE_PEEKTEXT, pid, addr, 0)返回0,errno为0,说明读取成功,但内容是0。这通常意味着addr指向的内存页未被映射,或addr不是可读地址。

childre中,0x4008a6在.text段,应该可读。如果返回0,检查:

  • addr是否对齐:ptrace要求addr是sizeof(long)对齐(x86_64为8字节),0x4008a6末位是6,不是0或8,不对齐!正确地址应为0x4008a0或0x4008a8。objdump显示raise在0x4008a6,但PTRACE_PEEKTEXT读的是8字节块,0x4008a6属于0x4008a0-0x4008a7块,所以读0x4008a0才能拿到包含raise指令的8字节。

注意:PTRACE_PEEKTEXT读取的是addr所在8字节块的起始地址。addr=0x4008a6,实际读0x4008a0。所以代码中应传入addr & ~7(按8字节对齐)。

5.3gdb中set follow-fork-mode child无效:gdb仍跟父进程

gdb默认follow-fork-mode为parent,即`

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 15:02:56

SpringBoot+Vue+MyBatis+MySQL健康管理系统实战

前几年做这类信息管理系统的项目特别多&#xff0c;很多刚入门的同学第一次接触企业级开发就是从一套“SpringBoot Vue MyBatis MySQL”的前后端分离项目开始的。这个项目就是一套非常典型的例子&#xff1a;人员健康信息登记、每日打卡、出入审批、异常上报、统计分析&…

作者头像 李华
网站建设 2026/10/2 15:02:51

Excel Range.Value数组:VBA与VB.NET的差异与避坑指南

做Excel二次开发的人&#xff0c;十有八九都写过或者抄过这行代码&#xff1a;arr Range("A1:C10").Value。在VBA里这行代码贼好用&#xff0c;一次把10行3列的数据打包进内存&#xff0c;循环、查找、统计都快得飞起。可是当你把同样的思路搬到VB.NET&#xff0c;打…

作者头像 李华
网站建设 2026/10/2 15:02:33

基于Spring Boot的南京特色美食小吃商城系统开发实战

每年三四月份&#xff0c;总有一批计算机专业的同学对着毕业设计选题发愁。Java方向绕不开Spring Boot&#xff0c;商城系统又是永远的“经典款”&#xff0c;但纯做电商又容易被老师一句“没有新意”打回来。这个题目——“基于Spring Boot框架的南京特色美食小吃商城系统”&a…

作者头像 李华
网站建设 2026/10/2 15:02:13

SpringBoot+Vue商城系统实战:从数据库建模到订单库存与部署全复盘

这个项目算是我这两年做过最完整的一个个人研发项目了。名字叫米家商城&#xff0c;其实是从零搭出来的前后端分离商城系统——前台用户商城加后台管理系统一套全通&#xff0c;后端SpringBoot MyBatis MySQL&#xff0c;前端Vue全家桶。源码量不算小&#xff0c;业务复杂度也…

作者头像 李华
网站建设 2026/10/2 15:02:08

输电线路红外过热检测数据集:VOC/YOLO双格式2253张实战解析

简介&#xff1a;面向电力设备巡检、计算机视觉目标检测研究者的输电线路红外过热检测数据集&#xff0c;针对输电线路运行中红外过热隐患难以快速定位的问题&#xff0c;提供精细标注数据&#xff0c;可直接用于目标检测模型的训练、验证与对比研究&#xff0c;也适合作为高校…

作者头像 李华
网站建设 2026/10/2 15:02:04

MindSpore字节码虚拟机:动态张量计算的实时编译内核

1. 这不是又一个“虚拟机JIT”的老故事&#xff0c;而是张量计算范式迁移的底层支点 你打开VSCode&#xff0c;选中MindSpore内核&#xff0c;写完一段 nn.Cell 定义&#xff0c;点击运行——几毫秒后&#xff0c;GPU上已开始执行融合后的算子流水线。你没手动写CUDA核&…

作者头像 李华