手書きアセンブリ、inline asm、seccompのデバッグ、または逆アセンブリを読む際、最も混乱しやすいのは以下の3つの異なるルールセットです。
- System V AMD64 C ABI:通常のユーザー空間関数が引数を渡す方法、戻り値の返却方法、レジスタの保存方法。
- Linux x86-64 syscall ABI:ユーザー空間がシステムコール番号と最大6つの引数をカーネルに渡す方法。
- CPUの
SYSCALL/SYSRET 命令のセマンティクス:特権レベルの切替時にハードウェアが実際にどのレジスタを変更するか。
本稿ではこれら3層をそれぞれ解説し、ネイティブな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引数は rcx ではなく r10 なのか
通常の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はinline 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
最初の呼び出しのフラグは第4引数であり、r10に入る必要があります。fd=-1 と offset=0 はそれぞれ r8、r9 にあります。匿名マッピングはゼロ初期化されたメモリを返すため、この例では先頭と末尾のバイトを書き込んでから解放します。長さが0の2回目のmmapは -EINVAL を返すはずであり、呼び出し元の errno を設定してはいけません。mmap(2)
エラー検出は、まず unsigned long に変換し、-4095 の符号なし表現と比較して、上位の予約領域をエラーとして解釈します。アドレス結果をチェックする前に有効なポインタとして逆参照しないでください。また、errno がカーネルによって自動的に設定されるとは限らない点にも注意してください。それはlibcのラッパー層の役割です。raw6 はこの例での同期呼び出しのデモンストレーションに過ぎず、スタック/制御フローを変更する clone、rt_sigreturn などのインターフェースに修正なしで使用するものではありません。syscall(2)
ローカルレジスタ変数と拡張asm制約を組み合わせることで、入力を指定されたレジスタに配置します。宣言自体がレジスタの永続的な独占を意味するわけではありません。そのため、パラメータ計算を完了してから直ちにasmを実行し、その間に呼び出し元保存レジスタを上書きする可能性のある関数呼び出しを挿入してはいけません。GCC ローカルレジスタ変数
戻り値は呼び出しごとに解釈する必要がある
| 元の結果 | 対応する状況 | 呼び出し側の次のステップ |
| mmap がエラーでないアドレスを返す | マッピングが確立された | 要求された長さおよび権限に従って使用する |
| read が 0 を返す | 通常ファイル/バイトストリームではEOFの可能性あり | 読み取りを終了し、-errno として解釈しない |
| write が正の値を返すが、要求より少ない | バイトの一部が送信済み | ポインタと残りの長さを進める |
| -EINTR | この呼び出しが中断され、エラーとして報告された | そのインターフェースのセマンティクスに従って再試行するかどうかを決定 |
| -EAGAIN | 現在即時に完了できない | 準備ができるまで待つか、スケジューラに返してビジーウェイトを避ける |
例えば6バイトを書き込む場合、2、-EINTR、4 と順に返された場合、2回目の呼び出しでポインタをさらに2進めてはいけません。正の戻り値のみが今回の進捗を確認します。本稿末尾のアセンブリループでは rsi/rdx を保持し、-EINTR に遭遇した場合は残りの部分に対して再試行し、短書き込みの場合のみこれらのレジスタを更新します。
rcx の2つの正体は同時に保持できない
ここでは2つのレジスタ表を暗記する必要はありません。第4引数が rcx→r10 の経路を移動し、ハードウェアによる rcx の上書きを観察すれば、なぜ2つのABIが接続しなければならないのかがわかります。RFLAGS* は命令が規定する処理後のフラグ値を示し、RFの詳細は次のセクションで説明します。
図を準備しています
rcx の2つの正体は同時に保持できないC ABI は rcx を使用して第4引数を渡すが、SYSCALL は rcx を上書きし、Linux は r10 から第4引数を読み取る。
{"id":"x86-fourth-argument","title":"rcx の2つの正体は同時に保持できない","summary":"C ABI は rcx を使用して第4引数を渡すが、SYSCALL は rcx を上書きし、Linux は r10 から第4引数を読み取る。","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":"第4引数の値","before":"SYSCALL 実行前","correct":"要求と一致","heading":"rcx の2つの正体は同時に保持できない","intro":"第4引数を変更し、正しい搬送と搬送漏れのエントリレジスタを比較する。","move":"mov rcx → r10 を実行","received":"カーネルが読み取った第4引数","regs":"境界両側のレジスタ","reset":"リセット","scope":"ネイティブなLinux x86-64 LP64、例はlibc syscall() の可変引数エントリではなく直接の六引数アセンブリラッパー。r10 の初期値は意図的に99に設定され、戻りアドレスは固定の0x401002。","scopeLabel":"モデル範囲と規約","verdict":"引数の照合","wrong":"古い r10 を取得"}}
2. SYSCALL 命令は何を行うか
64ビットモードで SYSCALL を実行すると、CPUは主に以下のことを完了します。
重要な結論は2つあります。
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のスクラッチスロットであり、ハードウェアによって自動的にスタックが切り替わる従来の sp0 フローではありません。順序も重要です。まずユーザー rsp を保存し、次にカーネルページテーブルとカーネルスタックに切り替え、最後にカーネルスタック上で struct pt_regs を構築します。
したがって、ユーザー空間のred zoneは通常のsyscallエントリでのレジスタフレームによって上書きされません。フレームはカーネルスタック上にあり、「ユーザー rsp の下」にはありません。
4. 戻り値:SYSRET 高速パスと IRET フォールバック
システムコール終了後、Linuxは戻りコンテキストが SYSRET に適しているかどうかをチェックします。アドレス、セグメントレジスタ、フラグビットなどの条件がすべて安全な場合にのみ高速パスを使用し、そうでない場合は IRET を使用します。
これは単なるパフォーマンスの選択ではありません。AMDとIntelの SYSRET は、非canonicalアドレスなどの境界条件に対して扱いにくい動作が存在するため、カーネルはまずユーザー制御可能な pt_regs を検証する必要があります。
ユーザー空間から観察すると、2つの戻りパスは同じsyscall ABIを満たす必要があります。カーネルが必ず SYSRET を使用することに依存してはいけません。
5. Red zone とシステムコール
System V AMD64 ABIでは、ユーザー rsp の下に128バイトのred zoneが定義されています。リーフ関数は rsp を調整せずにこの領域を一時的に使用できます。
3つの状況を区別する必要があります。
- 通常のsyscall/割り込みによるカーネル進入:Linuxはカーネルスタックに切り替え、エントリフレームはユーザーred zoneに書き込みません。
- ユーザー空間へのシグナル配信:カーネルはシグナルフレームを構築する際に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、および特定バージョンのカーネル実装を混同することがなくなります。