写手工汇编、inline asm、调试 seccomp 或阅读反汇编时,最容易混淆的是三套不同规则:
- System V AMD64 C ABI:普通用户态函数怎样传参、返回和保存寄存器。
- Linux x86-64 syscall ABI:用户态怎样把系统调用号和最多 6 个参数交给内核。
- CPU 的
SYSCALL/SYSRET 指令语义:硬件在特权级切换时实际改了哪些寄存器。
本文分别解释这三层,讨论原生 LP64 ABI,不包含 x32 ABI,并以 Linux v6.18 的 x86-64 入口代码为实现示例。内核实现会演进;复制源码片段时应注明版本,而不是把某个版本的细节当成永久 ABI。
1. Linux syscall 寄存器约定
| 用途 | 寄存器 |
| 系统调用号 | rax |
| 参数 1–3 | rdi, rsi, rdx |
| 参数 4–6 | r10, r8, r9 |
| 返回值 | rax |
| 硬件破坏 | rcx, r11 |
典型调用:
mov $1, %rax # __NR_write
mov $1, %rdi # fd = STDOUT_FILENO
lea message(%rip), %rsi
mov $6, %rdx
syscall
对这里的 write,成功返回写入字节数,失败返回 -errno。原生 Linux ABI 用 -4095 到 -1 表示错误;不能把所有系统调用的成功结果一概解释为字节数。glibc 包装函数(包括公共的 syscall())会把内核错误转换为 -1,并设置线程局部的 errno。
为什么第 4 个参数是 r10,而不是 rcx
普通 System V 函数的第 4 个整数参数使用 rcx;但 SYSCALL 指令必须把用户态返回地址写入 rcx。因此 Linux syscall ABI 把第 4 个参数移到 r10。
这也是 inline asm 必须声明 rcx 和 r11 clobber 的原因:
static inline long raw_write(int fd, const void *buf, unsigned long count)
{
register long rax __asm__("rax") = 1;
register long rdi __asm__("rdi") = fd;
register long rsi __asm__("rsi") = (long)buf;
register long rdx __asm__("rdx") = count;
__asm__ volatile (
"syscall"
: "+a"(rax)
: "D"(rdi), "S"(rsi), "d"(rdx)
: "rcx", "r11", "memory"
);
return rax;
}
对于 4–6 个参数,需要显式绑定 r10、r8、r9。生产代码通常应优先调用 libc;raw syscall 会绕过取消点、兼容封装和 libc 的错误处理。
完整六参数例子:申请一页匿名内存再释放
保存为 raw-mmap.c,在原生 Linux x86-64 上使用 GCC 或 Clang 编译。此例保留 libc 用于打印和查询页大小,但 mmap/munmap 通过内联 syscall 执行。SYS_* 宏来自本机目标头文件,避免把某个调用号误搬到另一种 ABI。
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
static long raw6(long number, long a1, long a2, long a3,
long a4, long a5, long a6) {
register long r10 __asm__("r10") = a4;
register long r8 __asm__("r8") = a5;
register long r9 __asm__("r9") = a6;
long result;
__asm__ volatile("syscall"
: "=a"(result)
: "0"(number), "D"(a1), "S"(a2), "d"(a3),
"r"(r10), "r"(r8), "r"(r9)
: "rcx", "r11", "memory");
return result;
}
static int raw_error(long result) {
return (unsigned long)result >= (unsigned long)-4095L;
}
int main(void) {
long size = sysconf(_SC_PAGESIZE);
if (size <= 0) return 1;
long address = raw6(SYS_mmap, 0, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (raw_error(address)) {
fprintf(stderr, "mmap error=%ld\n", -address);
return 1;
}
unsigned char *page = (unsigned char *)address;
page[0] = 7;
page[size - 1] = 9;
int valid = page[0] == 7 && page[size - 1] == 9;
long released = raw6(SYS_munmap, address, size, 0, 0, 0, 0);
if (raw_error(released) || !valid) return 1;
errno = 0;
long bad = raw6(SYS_mmap, 0, 0, PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int unchanged = errno == 0;
if (bad != -EINVAL || !unchanged) return 1;
printf("page checked; raw error=%ld; errno unchanged=%d\n",
-bad, unchanged);
return 0;
}
cc -std=gnu11 -Wall -Wextra -O2 raw-mmap.c -o raw-mmap
./raw-mmap
objdump -d -Mintel raw-mmap
第一次调用的 flags 是第 4 参数,必须进入 r10;fd=-1 和 offset=0 分别在 r8、r9。匿名映射返回零初始化内存,本例写入首尾字节后释放。长度为零的第二次 mmap 应返回 -EINVAL,并且不替调用者设置 errno。mmap(2)
错误检测先转换为 unsigned long,再与 -4095 的无符号表示比较,把顶端保留区间解释为错误。不要把地址结果在检查前当成有效指针解引用,也不要认为 errno 会被内核自动填写;那是 libc 包装层的工作。raw6 只是本例同步返回调用的演示,不可不加修改地用于改变栈/控制流的 clone、rt_sigreturn 等接口。syscall(2)
局部 register 变量配合扩展 asm 约束,把输入安排在指定寄存器中;声明本身不代表永久独占寄存器。因此先完成参数计算,再紧接着执行 asm,不能在中间插入可能覆盖调用者保存寄存器的函数调用。GCC 局部寄存器变量
返回值还要按具体调用解释
| 原始结果 | 对应情境 | 调用者下一步 |
| mmap 返回非错误地址 | 建立了映射 | 按申请长度与权限使用 |
| read 返回 0 | 对普通文件/字节流可能是 EOF | 结束读取,不能当成 -errno |
| write 返回正数但小于请求 | 已提交部分字节 | 推进指针和剩余长度 |
| -EINTR | 此次调用被中断并报告错误 | 按该接口语义决定重试 |
| -EAGAIN | 当前无法立即完成 | 等待就绪或交还调度,避免忙循环 |
例如要写 6 字节,依次返回 2、-EINTR、4,第二次不能把指针再推进 2:只有正返回值确认了本次进展。本文末尾的汇编循环保留 rsi/rdx,遇到 -EINTR 重试当前剩余部分,短写才更新这两个寄存器。
rcx 的两种身份不能同时保留
这里不需要背诵两张寄存器表:让第四参数沿着 rcx→r10 的路径移动,再观察硬件对 rcx 的覆盖,就能看见两种 ABI 为什么必须衔接。RFLAGS* 表示指令规定处理后的标志值,RF 的细节见下一节。
正在呈现知识画面
rcx 的两种身份不能同时保留C ABI 用 rcx 传第四参数,SYSCALL 覆盖 rcx,Linux 从 r10 读取第四参数。
{"id":"x86-fourth-argument","title":"rcx 的两种身份不能同时保留","summary":"C ABI 用 rcx 传第四参数,SYSCALL 覆盖 rcx,Linux 从 r10 读取第四参数。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"arg\" data-i18n=\"arg\"></label><output for=\"arg\" id=\"arg-value\"></output><input id=\"arg\" type=\"range\" min=\"0\" max=\"64\" value=\"34\" step=\"1\"></div><label class=\"check\"><input type=\"checkbox\" id=\"move\" checked><span data-i18n=\"move\"></span></label><section class=\"panel\"><h3 data-i18n=\"regs\"></h3><div id=\"regs\"></div></section><div class=\"metrics\"><div><span data-i18n=\"received\"></span><strong id=\"received\"></strong></div><div><span data-i18n=\"verdict\"></span><strong id=\"verdict\"></strong></div></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#regs .tr{grid-template-columns:.6fr 1fr 1fr}#regs .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;}));}function draw(){const n=+$('#arg').value,mv=$('#move').checked,r=mv?n:99;set('arg-value',n);table('regs',[['',t('before'),t('after')],['rcx',n,'0x401002'],['r10',r,r],['r11','…','RFLAGS*']]);set('received',r);set('verdict',t(mv?'correct':'wrong'));}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"after":"硬件进入后","arg":"第四参数值","before":"执行 SYSCALL 前","correct":"与请求一致","heading":"rcx 的两种身份不能同时保留","intro":"修改第四参数,对比正确搬运与漏搬运的入口寄存器。","move":"执行 mov rcx → r10","received":"内核读取的第四参数","regs":"边界两侧的寄存器","reset":"重置","scope":"原生 Linux x86-64 LP64,示例为直接六参数汇编封装而非 libc syscall() 的变参入口。r10 初值故意设为99,返回地址固定0x401002。","scopeLabel":"模型范围与约定","verdict":"参数核对","wrong":"取到了旧 r10"}}
2. SYSCALL 指令做什么
在 64 位模式下执行 SYSCALL 时,CPU 主要完成:
两个关键结论:
rcx 和 r11 被硬件占用,不能用于保存跨 syscall 的值。
SYSCALL 不会保存用户态 rsp,也不会自动切到内核栈。
上述图省略了 RF 的特殊处理:硬件先清除 RF,再把相应标志状态保存到 R11。RFLAGS 并非简单“保持不变”。内核在 IA32_FMASK 中配置要清除的标志位;Linux 的入口代码在建立安全状态后再管理中断。
3. Linux entry_SYSCALL_64
Linux v6.18 的 x86-64 入口核心顺序是:
# arch/x86/entry/entry_64.S(省略安全加固与注解)
entry_SYSCALL_64:
swapgs
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) # 暂存用户 rsp
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
pushq $__USER_DS
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
pushq %r11
pushq $__USER_CS
pushq %rcx
pushq %rax
# 继续构造 struct pt_regs,随后调用 do_syscall_64
这里的 tss.sp2 是 Linux 使用的 per-CPU scratch slot,不是传统意义上由硬件自动切栈的 sp0 流程。顺序也很重要:先保存用户 rsp,再切内核页表与内核栈,最后在内核栈上构造 struct pt_regs。
因此,用户态 red zone 不会被普通 syscall 入口上的寄存器帧覆盖;帧位于内核栈,而不是“用户 rsp 下方”。
4. 返回:SYSRET 快路径与 IRET 回退
系统调用结束后,Linux 会检查返回上下文是否适合 SYSRET。只有在地址、段寄存器和标志位等条件都安全时才走快路径;否则使用 IRET。
这样做不是单纯的性能选择。AMD 和 Intel 的 SYSRET 对非 canonical 地址等边界情况存在难处理的行为,内核必须先验证用户可控的 pt_regs。
从用户态观察,两条返回路径都应满足同一 syscall ABI;不能依赖内核一定使用 SYSRET。
5. Red zone 与系统调用
System V AMD64 ABI 在用户 rsp 下方定义 128 字节 red zone。叶子函数可以临时使用这块空间而不调整 rsp。
需要区分三种情况:
- 普通 syscall/中断进入内核:Linux 切换到内核栈,入口帧不写用户 red zone。
- 向用户态递送 signal:内核构造 signal frame 时会为 red zone 留出空间。
- 内核代码本身:内核以
-mno-red-zone 编译,不能依赖用户态 ABI 的 red zone。
手写 inline asm 时,"memory" clobber 只约束编译器的内存重排;它不代表 syscall 只会访问某个固定内存范围。传给内核的指针指向哪些缓冲区,仍由具体 syscall 定义。
6. vDSO 不是“在用户态执行 syscall”
vDSO 是内核映射进进程地址空间的一小段 ELF 代码。某些 libc API 可以先调用 vDSO,从内核维护的只读数据中计算结果,从而避免真正执行 SYSCALL。
常见符号包括:
__vdso_clock_gettime
__vdso_gettimeofday
__vdso_time
__vdso_getcpu
- 新内核上的
__vdso_getrandom
这些符号是否存在、能否完成请求取决于架构和内核版本;libc 必须准备回退到真正的 syscall。
检查当前进程的 vDSO:
rg '\[vdso\]' /proc/self/maps
strace -e trace=clock_gettime,gettimeofday /bin/date
7. int 0x80、sysenter 与 64 位 syscall
三者属于不同入口机制,不能只用一张固定“周期数”表比较:
| 机制 | 主要使用场景 | 入口配置 |
int 0x80 | 传统 i386 ABI;x86-64 下也有兼容入口 | IDT |
sysenter/sysexit | 32 位快速系统调用 | SYSENTER MSR |
syscall/sysret | 原生 x86-64 Linux syscall ABI | STAR/LSTAR/FMASK MSR |
实际延迟受 CPU 微架构、KPTI、缓解措施、虚拟化、频率和内核版本影响。需要性能数据时,应在目标机器上测量,并报告环境:
perf stat -r 20 ./syscall-benchmark
不要把某台旧 CPU 上的 50–70 cycles 当成跨平台事实。
8. 完整汇编示例
.section .rodata
message:
.ascii "hello\n"
.section .text
.global _start
_start:
lea message(%rip), %rsi
mov $6, %rdx
.write:
mov $1, %rax # __NR_write
mov $1, %rdi # stdout
syscall
cmp $-4, %rax # -EINTR:本次没有写入,重试
je .write
test %rax, %rax
jle .error # 负值或无进展;此例不等待 EAGAIN
add %rax, %rsi
sub %rax, %rdx
jnz .write # 短写:继续写剩余部分
xor %rdi, %rdi
jmp .exit
.error:
mov $1, %rdi
.exit:
mov $60, %rax # __NR_exit:退出当前线程
syscall
.section .note.GNU-stack,"",@progbits
示例是没有 libc 的单线程程序;原始 exit 退出当前线程,退出整个线程组是 exit_group。若 stdout 是已关闭读端的管道,默认的 SIGPIPE 也可能直接终止进程。
构建与观察:
as -o syscall.o syscall.S
ld -o syscall syscall.o
strace ./syscall
objdump -drwC -Mintel ./syscall
9. 权威资料
阅读顺序建议:先用 syscall(2) 确认 Linux ABI,再用处理器手册确认 SYSCALL/SYSRET 指令语义,最后对照目标内核版本的 entry_64.S。这样不会把 CPU 规则、用户态 C ABI 和某一版内核实现混为一谈。