When writing hand-crafted assembly, inline asm, debugging seccomp, or reading disassembly, the most confusing aspect is the existence of three different sets of rules:
System V AMD64 C ABI: How normal user-space functions pass arguments, return values, and save registers.
Linux x86-64 syscall ABI: How user-space passes the system call number and up to 6 arguments to the kernel.
CPU SYSCALL/SYSRET instruction semantics: Which registers the hardware actually modifies during privilege level transitions.
This article explains these three layers separately, discusses the native LP64 ABI, and does not cover the x32 ABI. The implementation example uses the x86-64 entry code from Linux v6.18. Kernel implementations evolve; when copying source code snippets, you should note the version number rather than treating details from a specific version as a permanent ABI.
For write here, a successful call returns the number of bytes written, while a failure returns -errno. The native Linux ABI uses -4095 to -1 to indicate errors; you cannot uniformly interpret all successful system call results as byte counts. glibc wrapper functions (including the public syscall()) convert kernel errors to -1 and set the thread-local errno.
Why is the 4th argument r10 instead of rcx?
For normal System V functions, the 4th integer argument uses rcx; however, the SYSCALL instruction must write the user-space return address into rcx. Therefore, the Linux syscall ABI moves the 4th argument to r10.
This is also why inline asm must declare rcx and r11 as clobbered:
For arguments 4–6, you need to explicitly bind r10, r8, and r9. Production code should generally prefer calling libc; raw syscalls bypass cancellation points, compatibility wrappers, and libc error handling.
Complete six-argument example: Allocate a page of anonymous memory and then free it
Save as raw-mmap.c and compile with GCC or Clang on native Linux x86-64. This example retains libc for printing and querying page size, but mmap/munmap are executed via inline syscalls. The SYS_* macros come from the native target headers to avoid accidentally moving a call number to a different ABI.
For the first call, the flags are the 4th argument and must go into r10; fd=-1 and offset=0 go into r8 and r9 respectively. Anonymous mappings return zero-initialized memory; this example writes the first and last bytes and then releases it. The second mmap with a length of zero should return -EINVAL and not set errno on behalf of the caller. mmap(2)
Error detection first casts the result to unsigned long and compares it with the unsigned representation of -4095, interpreting the top reserved range as an error. Do not dereference the address result as a valid pointer before checking, and do not assume errno will be automatically filled by the kernel; that is the job of the libc wrapper layer. raw6 is merely a demonstration of synchronous return calls in this example and should not be used without modification for interfaces like clone or rt_sigreturn that change the stack/control flow. syscall(2)
Local register variables combined with extended asm constraints place inputs in specific registers; the declaration itself does not represent permanent exclusive register ownership. Therefore, complete argument calculations first, then immediately execute the asm; do not insert function calls in between that might overwrite caller-saved registers. GCC Local Register Variables
Return values must be interpreted according to the specific call
Raw Result
Corresponding Situation
Caller's Next Step
mmap returns non-error address
Mapping established
Use according to requested length and permissions
read returns 0
EOF for regular files/byte streams
End reading, do not treat as -errno
write returns positive number less than requested
Partial bytes submitted
Advance pointer and remaining length
-EINTR
Call interrupted and reported error
Decide whether to retry based on interface semantics
-EAGAIN
Cannot complete immediately
Wait for readiness or yield to scheduler, avoid busy loop
For example, if you want to write 6 bytes and the returns are sequentially 2, -EINTR, 4, you should not advance the pointer by 2 again on the second call: only positive return values confirm progress in that specific attempt. The assembly loop at the end of this article retains rsi/rdx and retries the current remainder upon -EINTR, updating these two registers only for short writes.
The two identities of rcx cannot be preserved simultaneously
There is no need to memorize two separate register tables: letting the 4th argument move along the rcx→r10 path and then observing the hardware's overwrite of rcx reveals why the two ABIs must connect. RFLAGS* represents the flag values after instruction-defined processing; see the next section for details on RF.
Preparing the visual
The two identities of rcx cannot be preserved simultaneously
The C ABI uses rcx to pass the 4th argument, SYSCALL overwrites rcx, and Linux reads the 4th argument from r10.
{"id":"x86-fourth-argument","title":"The two identities of rcx cannot be preserved simultaneously","summary":"The C ABI uses rcx to pass the 4th argument, SYSCALL overwrites rcx, and Linux reads the 4th argument from 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":"After hardware entry","arg":"4th Argument Value","before":"Before executing SYSCALL","correct":"Matches Request","heading":"The two identities of rcx cannot be preserved simultaneously","intro":"Modify the 4th argument, comparing the entry registers for correct movement versus missed movement.","move":"Execute mov rcx → r10","received":"4th Argument Read by Kernel","regs":"Registers on both sides of the boundary","reset":"Reset","scope":"Native Linux x86-64 LP64, example is a direct six-argument assembly wrapper rather than a variadic entry for libc syscall(). r10 initial value is deliberately set to 99, return address fixed at 0x401002.","scopeLabel":"Model Scope and Conventions","verdict":"Parameter Verification","wrong":"Retrieved Old r10"}}
2. What Does the SYSCALL Instruction Do?
When executing SYSCALL in 64-bit mode, the CPU primarily performs:
Two key conclusions:
rcx and r11 are occupied by hardware and cannot be used to save values across syscalls.
SYSCALLdoes not save user-space rsp, nor does it automatically switch to the kernel stack.
The diagram above omits special handling for RF: the hardware first clears RF, then saves the corresponding flag status to R11. RFLAGS is not simply "kept unchanged." The kernel configures the flags to be cleared in IA32_FMASK; the Linux entry code manages interrupts after establishing a safe state.
3. Linux entry_SYSCALL_64
The core sequence of the x86-64 entry in Linux v6.18 is:
Here, tss.sp2 is a per-CPU scratch slot used by Linux, not the traditional sp0 flow where the hardware automatically switches stacks. The order is also important: save user rsp first, then switch kernel page tables and kernel stack, and finally construct struct pt_regs on the kernel stack.
Therefore, the user-space red zone is not overwritten by the register frame on a normal syscall entry; the frame is located on the kernel stack, not "below the user rsp."
4. Return: SYSRET Fast Path and IRET Fallback
After a system call, Linux checks whether the return context is suitable for SYSRET. The fast path is taken only if conditions such as address, segment registers, and flag bits are all safe; otherwise, IRET is used.
This is not merely a performance choice. AMD and Intel's SYSRET have difficult-to-handle behaviors for edge cases such as non-canonical addresses; the kernel must first verify user-controllable pt_regs.
From the user-space perspective, both return paths should satisfy the same syscall ABI; you cannot rely on the kernel definitely using SYSRET.
5. Red Zone and System Calls
The System V AMD64 ABI defines a 128-byte red zone below the user rsp. Leaf functions can temporarily use this space without adjusting rsp.
Three situations need to be distinguished:
Normal syscall/interrupt entry into kernel: Linux switches to the kernel stack; the entry frame does not write to the user red zone.
Delivering a signal to user space: The kernel allocates space for the red zone when constructing the signal frame.
Kernel code itself: The kernel is compiled with -mno-red-zone and cannot rely on the user-space ABI's red zone.
When writing hand-crafted inline asm, the "memory" clobber only constrains compiler memory reordering; it does not imply that the syscall only accesses a fixed memory range. Which buffers the pointers passed to the kernel point to are still defined by the specific syscall.
6. vDSO is Not "Executing Syscall in User Space"
The vDSO is a small piece of ELF code mapped by the kernel into the process address space. Some libc APIs can first call the vDSO to calculate results from read-only data maintained by the kernel, thereby avoiding the actual execution of SYSCALL.
Common symbols include:
__vdso_clock_gettime
__vdso_gettimeofday
__vdso_time
__vdso_getcpu
__vdso_getrandom on newer kernels
The existence of these symbols and their ability to fulfill requests depend on the architecture and kernel version; libc must be prepared to fall back to a real syscall.
Check the vDSO for the current process:
rg'\[vdso\]' /proc/self/maps# In a glibc environment, you can observe whether the actual call still enters the kernelstrace -e trace=clock_gettime,gettimeofday /bin/date
7. int 0x80, sysenter, and 64-bit syscall
The three belong to different entry mechanisms and cannot be compared using a fixed "cycle count" table:
Mechanism
Primary Use Case
Entry Configuration
int 0x80
Traditional i386 ABI; compatibility entry under x86-64
IDT
sysenter/sysexit
32-bit fast system call
SYSENTER MSR
syscall/sysret
Native x86-64 Linux syscall ABI
STAR/LSTAR/FMASK MSR
Actual latency is affected by CPU microarchitecture, KPTI, mitigation measures, virtualization, frequency, and kernel version. When performance data is needed, it should be measured on the target machine, and the environment should be reported:
perf stat -r 20 ./syscall-benchmark
Do not treat 50–70 cycles on an old CPU as a cross-platform fact.
8. Complete Assembly Example
.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: nothing written this time, retry
je .write
test %rax, %rax
jle .error # Negative value or no progress; this example does not wait for EAGAIN
add %rax, %rsi
sub %rax, %rdx
jnz .write # Short write: continue writing the remainder
xor %rdi, %rdi
jmp .exit
.error:
mov $1, %rdi
.exit:
mov $60, %rax # __NR_exit: Exit current thread
syscall
.section .note.GNU-stack,"",@progbits
The example is a single-threaded program without libc; the original exit exits the current thread, and exit_group exits the entire thread group. If stdout is a pipe with the read end closed, the default SIGPIPE may also terminate the process directly.
Recommended reading order: First confirm the Linux ABI using syscall(2), then confirm the SYSCALL/SYSRET instruction semantics using the processor manual, and finally cross-reference entry_64.S for the target kernel version. This prevents confusing CPU rules, user-space C ABI, and a specific kernel implementation.