ptrace 让 tracer 观察并控制受权限规则约束的 tracee 线程,例如在停止点读取寄存器、检查内存、继续运行或单步。Linux 原生调试器和系统调用跟踪器常使用它,但整个进程、多线程组与单个被跟踪线程不是同一个对象。
/proc/PID/mem、process_vm_readv 和 seccomp user notification 各解决部分问题,不能直接视为可互相替换的“新一代 ptrace”。
建立跟踪关系后,还要等到可操作状态
PTRACE_TRACEME 由 tracee 调用,请求由父进程跟踪,并不表示任意其他进程此后都有权限。PTRACE_ATTACH 附加并触发相应停止流程,tracer 还需 wait 确认;PTRACE_SEIZE 建立关系而不立即停止,之后可使用 PTRACE_INTERRUPT 请求停止。
ptrace 请求通常以 TID 指定线程。一个线程只能有一个 tracer,附加某个 TID 不会自动把所有现有和未来线程都完整纳入控制。需要枚举现有线程,并按需求设置 clone、fork、exec、exit 等事件选项。
一些读写请求要求 tracee 已处于合适的 ptrace-stop。仅看到进程“似乎不运行”或刚刚调用了 ATTACH,不足以保证马上读取寄存器成功。ptrace(2)
停止状态是调试器的核心
系统调用入口和出口之间可能夹杂信号、exec、退出或其他事件。真实循环要先区分 WIFEXITED、WIFSIGNALED、WIFSTOPPED,再解释具体停止类型,而不是无条件把两次 wait 当作一对。
PTRACE_O_TRACESYSGOOD 可让系统调用停止的信号标识带上 0x80,便于与普通 SIGTRAP 区分。支持时 PTRACE_GET_SYSCALL_INFO 能取得更明确的入口、出口等信息,减少直接依赖架构寄存器布局;仍要处理重启系统调用和事件转换。
信号递送停止后,以 0 继续可能抑制该信号;传入原信号则让其继续递送。不能对所有停止一律传 0,然后认为跟踪对程序语义完全透明。group-stop、ptrace 事件停止和普通信号停止的处理也不同。
分类与配对要分别做
先按 wait 状态区分退出和停止,再从停止信息识别事件;不要先翻转“进入/退出”布尔值,再想办法忽略信号。使用 PTRACE_GET_SYSCALL_INFO 时也要检查返回的 op,只有明确的 ENTRY/EXIT 才进入相应处理,SECCOMP 与 NONE 不能硬塞进普通配对。ptrace 停止与 syscall info
下面是已经分类好的离线事件模型,不调用 ptrace。它演示两个线程交错时,一个全局 in_syscall 布尔值为什么不够;线程结束但没有普通 syscall exit 时,将尚未完成的观察另行记录。
pending = {}
completed = []
incomplete = []
signal_stops = []
events = [
(11, "entry", "read"),
(12, "entry", "write"),
(11, "signal", "SIGUSR1"),
(12, "exit", 3),
(11, "exit", 8),
(12, "entry", "exit_group"),
(12, "terminated", 0),
]
for tid, kind, value in events:
if kind == "entry":
assert tid not in pending
pending[tid] = value
elif kind == "exit":
assert tid in pending
completed.append((tid, pending.pop(tid), value))
elif kind == "signal":
signal_stops.append((tid, value))
elif kind == "terminated":
if tid in pending:
incomplete.append((tid, pending.pop(tid)))
assert completed == [(12, "write", 3), (11, "read", 8)]
assert signal_stops == [(11, "SIGUSR1")]
assert incomplete == [(12, "exit_group")]
assert not pending
模型的 assert 是为了暴露输入中断裂的配对。实际 tracer 不能遇到附加时已在系统调用内、exec 或重启路径就直接当作程序错误退出;需要按观察起点和事件语义重建状态,并给无法可靠关联的记录加标记。耗时统计也只能覆盖实际配对的区间,不能把缺失出口的调用记为零耗时。
| 停止分类 | 适用动作 | 不应照搬的动作 |
| 明确 syscall entry/exit | 读取相应调用信息,按 TID 关联 | 每次 SIGTRAP 都翻转配对 |
| signal-delivery-stop | 读取信号信息,决定是否转交 | 所有信号一律丢弃 |
| ptrace event | 更新线程、exec 或退出状态 | 把事件码当作普通系统调用结果 |
| group-stop | 按所用附加方式处理作业控制 | 无条件 CONT,改变原有停止语义 |
| 已退出/被终止 | 回收线程状态与未完成记录 | 继续读已不存在的寄存器 |
一次信号停止,会打乱 syscall 配对吗?
让 A 进入 syscall,随后让 B 进入,再给 A 加一次信号停止,最后先结束 B 的调用。两条调用仍能正确配对;如果用全局进入/退出开关,第二条入口就会被误认成出口。线程终止时未配对的记录应标为不完整,不能记为零耗时。
正在呈现知识画面
一次信号停止,会打乱 syscall 配对吗?syscall entry/exit 需要先分类再按 TID 配对。信号停止不完成调用,线程退出会留下未完成观察。
{"id":"ptrace-per-thread-pairing","title":"一次信号停止,会打乱 syscall 配对吗?","summary":"syscall entry/exit 需要先分类再按 TID 配对。信号停止不完成调用,线程退出会留下未完成观察。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"two\"><section class=\"card\"><header>TID A</header><button data-tid=\"A\" data-op=\"entry\" data-i18n=\"entry\"></button><button data-tid=\"A\" data-op=\"signal\" data-i18n=\"signal\"></button><button data-tid=\"A\" data-op=\"exit\" data-i18n=\"exit\"></button><button data-tid=\"A\" data-op=\"death\" data-i18n=\"death\"></button></section><section class=\"card\"><header>TID B</header><button data-tid=\"B\" data-op=\"entry\" data-i18n=\"entry\"></button><button data-tid=\"B\" data-op=\"signal\" data-i18n=\"signal\"></button><button data-tid=\"B\" data-op=\"exit\" data-i18n=\"exit\"></button><button data-tid=\"B\" data-op=\"death\" data-i18n=\"death\"></button></section></div><section class=\"panel\"><h3 data-i18n=\"records\"></h3><div id=\"records\"></div></section><div class=\"metrics\"><div><span data-i18n=\"open\"></span><strong id=\"open\"></strong></div><div><span data-i18n=\"done\"></span><strong id=\"done\"></strong></div><div><span data-i18n=\"lost\"></span><strong id=\"lost\"></strong></div></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.card button{margin:4px 0}#records .tr{grid-template-columns:.6fr 2fr}#records .tr>*{min-height:4em}.metrics strong{font-size:17px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let threads={A:{},B:{}},done=0,lost=0,serial=0;function draw(){table('records',Object.entries(threads).map(([n,s])=>[n,s.dead?t('ended'):s.call?'call #'+s.call:t('idle')]));set('open',Object.values(threads).filter(s=>s.call).length);set('done',done);set('lost',lost);document.querySelectorAll('[data-op]').forEach(e=>{const s=threads[e.dataset.tid];e.disabled=!!s.dead||(e.dataset.op==='entry'&&!!s.call)||(e.dataset.op==='exit'&&!s.call)});}document.querySelectorAll('[data-op]').forEach(e=>e.onclick=()=>{const s=threads[e.dataset.tid],op=e.dataset.op;if(op==='entry')s.call=++serial;if(op==='exit'){delete s.call;done++}if(op==='death'){if(s.call)lost++;delete s.call;s.dead=true}draw()});$('#reset').onclick=()=>{threads={A:{},B:{}};done=lost=serial=0;draw()};draw();","audio":false,"strings":{"death":"线程结束","done":"已配对","ended":"线程已结束","entry":"syscall 入口","exit":"syscall 出口","heading":"一次信号停止,会打乱 syscall 配对吗?","idle":"无未完成调用","intro":"向两个线程独立加入入口、信号停止和出口,观察每线程未完成记录。","lost":"结束时未完成","open":"未完成调用","records":"每线程观察","reset":"重置","scope":"离线的已分类事件;不调用 ptrace,不模拟 exec、重启或中途附加。入口和出口只在模型允许的状态可选。","scopeLabel":"模型范围与约定","signal":"信号停止"}}
系统调用观察与扰动
PTRACE_SYSCALL 请求在相关系统调用边界停止。入口可读调用号和参数,出口可读返回值;raw 返回的负 errno 与 libc 包装层的 -1 + errno 不同。x86-64 的系统调用第 4 参数在 r10,不是普通 C 调用的 rcx;arm64 有自己的 ABI。
每个停止都涉及内核处理、调度 tracer 和读取信息,频繁系统调用会被明显扰动。不能把开销固定成精确四次上下文切换,更不能用被跟踪运行时间直接替代正常性能。
strace 可过滤显示内容,但显示过滤与是否仍发生 ptrace 停止不是同一层次;某些优化模式会借助 seccomp 减少无关停止。性能热点可以先用采样或 BPF 聚合定位,再用细粒度跟踪验证具体行为。
读寄存器与内存
PTRACE_GETREGS 并非所有架构都提供相同接口。PTRACE_GETREGSET 配合相应 note 类型和 iovec 更适合按寄存器集取状态,仍需依据目标架构解释字节布局。
PEEK 类操作成功读出的数据本身可能等于 -1,因此必须结合 errno:
#include <errno.h>
#include <stddef.h>
#include <sys/ptrace.h>
#include <sys/types.h>
int peek_word(pid_t tid, const void *address, long *out) {
errno = 0;
long value = ptrace(PTRACE_PEEKDATA, tid, address, NULL);
if (value == -1 && errno != 0) return -1;
*out = value;
return 0;
}
调用方须先获得权限并使目标停在适当状态。按机器字读内存还涉及长度、对齐、页边界和目标字长;一个 word 成功不能推出旁边页也可读。
软件断点与硬件断点
在 x86 上,软件断点常用单字节 INT3 替换目标指令开头。命中后调试器识别断点、恢复原指令,并按架构规则调整 PC(INT3 停止位置通常已越过该字节),执行原指令后再重新插入断点。
只写“恢复后继续”会漏掉重新插入和多线程竞争:另一线程可能在断点暂时移除期间经过该位置。调试器需要暂停协调、单步或其他策略。arm64 的 BRK 编码及 PC 处理不同,不能把 0xCC 泛化到所有目标。
硬件执行断点、观察点依架构调试资源实现,有数量、长度和对齐限制。优化还可能让一个源码行对应多个地址,或某个变量没有可读取的内存位置;这不是单纯读错寄存器。
/proc/PID/mem 与 process_vm_readv
/proc/PID/mem 允许按目标虚拟地址通过文件偏移访问内存,打开受 ptrace 访问模式与其他安全规则约束,并非绕过权限。是否需先停止目标、哪些地址能访问,还取决于一致性要求和具体接口行为。proc_pid_mem(5)
process_vm_readv/writev 可在进程间批量传输内存,Linux 3.2 已提供,并非 5.x 才有。它们可能部分传输,不保证整个向量操作原子,也不替应用冻结目标;读取一个正被修改的链表,可能拼出从未同时存在过的字段组合。process_vm_readv(2)
批量读取适合降低访问开销,但不提供单步、断点或寄存器停止语义。访问权限与页面保护细节也不应按“新版内核就都允许”概括。
正的部分读取结果,只证明一个已复制前缀
假设本地目标缓冲容量 8192,process_vm_readv 返回 4096。只有前 4096 字节是本次成功复制的数据,后半段不能作为新快照解析;“返回非负”不足以证明整个结构完整。应在提交解析结果前,核对实际长度是否覆盖所需字段。
| 读取策略 | 可以改善什么 | 仍然解决不了什么 |
| 按页拆分远端 iovec | 将未知地址的故障边界限制在较小区域 | 整个对象跨页时的一致性 |
| 核对实际传输长度 | 避免解析未读入的尾部 | 目标在两次读取之间修改内容 |
| 先读头部再按长度读主体 | 限制分配规模与读取范围 | 头部与主体属于不同版本 |
| 停止所有会修改对象的线程 | 在已覆盖范围内减少并发修改 | 其他进程共享映射等外部修改者 |
process_vm_readv 的部分传输以 iovec 元素边界处理;读不知长度的字符串时,不要让一个远端 iovec 无边界地跨越未知页面。更关键的是,复制过程不提供应用对象的原子快照语义。process_vm_readv 传输约定
例如读取到旧的 length=100,又读到已经被替换的新指针,单独两个字段都可读,拼在一起却不一定曾经存在。若目标程序能够配合,使用目标自己生成并发布的不可变快照更容易定义一致性;若不能配合,就必须在结果中保留采集范围和一致性限制。
seccomp user notification:委托处理部分系统调用
seccomp 过滤器可让匹配的调用向监听者发出通知,由监督进程返回结果、错误,或在支持的情形下继续原调用、注入 fd。它适合特定系统调用的代理与虚拟化,不是通用用户指令调试器。
通知中出现的用户指针所指内存可能被其他线程修改。先读取路径做检查,再允许原系统调用继续使用同一指针,存在 TOCTOU:调用执行时内容可能已不同。通知 ID 有效性检查只能解决一部分生命周期问题,不能证明用户内存内容被冻结。
因此不能把 CONTINUE 方式当作对指针内容做安全授权的通用方案,也不能把监督端“检查/修改/批准”写成原子事务。应按需要复制、代理操作并返回受控资源,遵守接口的具体竞态处理规则。seccomp_unotify(2)
Yama 是附加限制,不替代基础权限
| ptrace_scope | 核心含义 |
| 0 | 经典权限检查;不是任何用户可跟踪任何进程 |
| 1 | 受限附加,通常要求适当祖先关系或显式授权关系;管理员能力例外仍有条件 |
| 2 | 仅具有相应 CAP_SYS_PTRACE 能力的管理者可执行受限跟踪操作 |
| 3 | 禁止相应附加与 TRACEME 跟踪;设置后不能再降低该值 |
具体还要结合凭证、dumpable、用户名字空间、其他 LSM 和 capability。不能把 2 写成“仅后代可跟踪”。现代 Linux 也不存在对 PID 1 在所有权限环境下一概禁止的简单规则。Yama 文档
诊断权限失败时,先确认父子关系、TID、凭证和系统策略,而不是直接修改全局限制。跟踪带特权执行的程序还可能改变 exec 时凭证提升行为,因此调试状态与正常运行状态可能不同。
选择观察接口
断点、单步与受控寄存器访问选调试器/ptrace;批量内存检查可结合 process_vm_readv;统计高频行为可考虑 perf/BPF;系统调用代理则按 seccomp 通知协议设计。所有方案都需明确观察范围和一致性,工具更快不代表证据自动更完整。
关键词:ptrace, tracee, TID, syscall stop, TRACESYSGOOD, GETREGSET, breakpoint, process_vm_readv, seccomp notification, Yama, TOCTOU