Seeing openat in strace does not necessarily mean the source code called openat; calling clock_gettime does not guarantee that a system call with the same name appears in the trace. The libc provides the semantics of the public interface, but internally it may choose other system calls, user-space fast paths, or combine multiple steps.
Understanding this boundary serves two purposes: when reading disassembly, you know where to look for argument and error translation; when optimizing, you can confirm whether you have preserved the semantics of the original API.
Three Interface Layers: The Same Name May Refer to Different Things
Layer
Example
Who Handles ABI and Errors
Public Library Function
read(fd, buf, n)
libc implements the full contract of the function
Generic Syscall Function
syscall(SYS_read, fd, buf, n)
libc arranges registers and translates errors; not equivalent to all semantics of read
Raw Instruction Wrapper
Inline assembly syscall / svc
Caller passes arguments according to architecture ABI and interprets raw results
In the raw ABIs of x86-64 and AArch64 Linux, error codes are -errno. The public syscall() has already converted errors to -1 and written to errno. Therefore, after seeing syscall(...) == -1, you should read errno; do not negate -1 again to treat it as an error number. syscall(2)
errno is thread-local state; successful returns do not require it to be cleared. Only interpret it when the interface return value indicates failure, and save it before subsequent calls that might overwrite errno. Not all C interfaces report errors via errno; for example, many pthread functions return error codes directly.
An invalid file descriptor causes the kernel to return EBADF. The application observes -1 and errno=EBADF; strace decodes the raw error into a human-readable form. printf itself may also generate a write; when analyzing the trace, distinguish this from the target call with fd=-1.
If you use raw_write from the x86-64 article, the return value will be directly -EBADF. Do not mix these two error conventions in a single algorithm.
Save Error Codes Together with the Return Value That Generated Them
Interface Convention
How to Judge
Situations Prone to Misinterpretation
read / write / syscall
Read errno after return value is -1
errno retains old errors on success
pthread_create, etc.
Check the error code returned directly by the function
Non-zero return but printing unrelated errno
Interfaces with ambiguous zero values (e.g., getauxval)
Clear errno first per documentation, then combine with result
Misjudging non-existence based solely on zero value
Raw syscall assembly
Judge based on architecture error range
Treating unconverted -errno as ordinary -1
For example, if the first write fails, and then you open a log file to record context, this open might also change errno. You should save saved_errno immediately after the failure, and then use the saved value to format the report. Setting errno=0 is not a fixed ritual before all calls; only do this when the interface convention requires eliminating ambiguity from old values.
Where does old errno go after success?
Fail once, then succeed: the result becomes 3 while errno can remain 9. errno is not a global error lamp reset by the kernel. Save it immediately when the corresponding return indicates failure.
Preparing the visual
Where does old errno go after success?
glibc syscall() converts raw -errno to -1 and sets errno; success need not clear errno. Interpret errno with the matching return value.
{"id":"libc-errno-pair","title":"Where does old errno go after success?","summary":"glibc syscall() converts raw -errno to -1 and sets errno; success need not clear errno. Interpret errno with the matching return value.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"fail\" data-i18n=\"fail\"></button><button id=\"intr\" data-i18n=\"intr\"></button><button id=\"success\" data-i18n=\"success\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"metrics\"><div><span data-i18n=\"raw\"></span><strong id=\"raw\"></strong></div><div><span data-i18n=\"returned\"></span><strong id=\"returned\"></strong></div><div><span data-i18n=\"errno\"></span><strong id=\"errno\"></strong></div><div><span data-i18n=\"use\"></span><strong id=\"use\"></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.metrics strong{font-size:17px}.metrics #use{min-height:5em}","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 raw=null,err=0;function draw(){set('raw',raw??'—');set('returned',raw===null?'—':raw<0?-1:raw);set('errno',err);set('use',t(raw===null?'pending':raw<0?'read':'ignore'));}for(const [id,n] of [['fail',-9],['intr',-4],['success',3]])$('#'+id).onclick=()=>{raw=n;if(n<0)err=-n;draw()};$('#reset').onclick=()=>{raw=null;err=0;draw()};draw();","audio":false,"strings":{"errno":"Current errno","fail":"write: EBADF","heading":"Where does old errno go after success?","ignore":"Success: ignore old error","intr":"write: EINTR","intro":"Run failed and successful wrapped calls and observe return value and thread-local errno.","pending":"No call yet","raw":"Raw kernel result","read":"Failure: save this error","reset":"Reset","returned":"Application result","scope":"Linux EBADF=9 and EINTR=4. This success path preserves errno; other functions may change it on success, so it is not a success-path failure signal.","scopeLabel":"Model scope and assumptions","success":"write: 3 bytes","use":"How to read errno here"}}
What Macros in Source Code Solve
The architecture layer of glibc arranges call numbers, input/output registers, and clobbers; the internal call layer preserves raw results, and the public wrapper layer performs errno translation. Specific macro names and expansion methods vary by version, architecture, and number of arguments; you cannot treat the expansion of an old version's macro as a permanent interface for glibc.
When reading inline asm, focus on checking: whether the output registers are correctly associated with the input call number, whether the fourth argument is placed in the register required by the target ABI, whether hardware-clobbered registers are listed in the clobber list, and whether memory visibility is accurately described.
asm volatile indicates that the assembly has side effects that cannot be arbitrarily removed; it is not itself a memory barrier. The "memory" clobber tells the compiler that the assembly may access memory not listed, limiting corresponding compiler reordering, and does not automatically generate CPU memory barriers. These constraints are a different layer of concern from C/C++ atomic memory orders. GCC Extended Asm
Thread Cancellation is Not "Check Before a Slow Call"
Interfaces like read and write have cancellation point semantics. The glibc read implementation goes through SYSCALL_CANCEL; the implementation needs to handle cancellation requests and the race windows between entering, blocking, and leaving the system call. Checking the flag only once before the call might result in receiving a cancellation request after the check and blocking permanently. glibc read implementation
Directly changing to syscall(SYS_read, ...) or raw assembly cannot be considered an equivalent replacement. It may bypass libc's cancellation mechanism, thereby changing thread stop and resource cleanup behavior. Conversely, using the public read does not mean the thread can be safely asynchronously cancelled at any arbitrary point; design resource lifecycles according to the conventions of cancellation types and cleanup handlers.
Observe Cancellation and Cleanup with Deterministic Ordering
Below, on glibc/Linux, we set up a pipe; the worker thread eventually prepares to call read; the main thread holds the write end but does not write, so there is no data, and EOF does not appear prematurely. Two barriers ensure that cleanup is registered first, then the cancellation request is issued, and finally cancellation is re-enabled. With deferred cancellation (default), the suspended request is processed at the cancellation point of the public read. Cancellation State
cc -std=c11 -Wall -Wextra -pthread cancel-read.c -o cancel-read./cancel-read
Expected output: canceled=1 cleaned=1. cleaned is written by the worker thread and read by the main thread after a successful join; this read point guarantees that the thread has ended; you cannot remove the join and have the main thread poll a plain int without synchronization.
pthread_cleanup_push/pop are macros in glibc that must be used in pairs, and their scopes must match. In this example, the cleanup handler owns the read end, and the main thread owns the write end; after separating the closing responsibilities, neither the cancellation path nor the normal path will close the same descriptor twice. To highlight the protocol, thread infrastructure errors terminate the example with abort; production code also needs to arrange resource cleanup in case of creation failures. Cleanup Handlers
Do not directly replace the read in this example with a raw system call to make a "faster" version: that would change the cancellation point semantics, and suspended requests might not terminate the wait according to this example's protocol. If you choose to manage stopping yourself, provide a clear wake-up channel for the wait and redesign the cleanup protocol, rather than just adding a boolean value outside the loop.
vDSO: Keep API, Replace Implementation Path
The kernel maps the vDSO into the process, and libc can resolve versioned symbols within it. For supported clocks, the clock_gettime path can read kernel-shared timing information and compute results in user space; when encountering unsupported environments or requests, the path may enter the kernel.
This is not a generic whitelist based on the numerical value of clock_id. Different architectures, time64 compatibility requirements, vDSO symbols, and their return errors all affect the implementation. You also cannot unconditionally ignore vDSO errors and call again: specific error propagation and fallback must conform to the interface. glibc clock_gettime, vdso(7)
Below, compare the two paths on native 64-bit Linux:
#define_GNU_SOURCE#include<stdio.h>#include<sys/syscall.h>#include<time.h>#include<unistd.h>intmain(void){struct timespec a, b;if(clock_gettime(CLOCK_MONOTONIC,&a)==-1){perror("clock_gettime");return1;}if(syscall(SYS_clock_gettime, CLOCK_MONOTONIC,&b)==-1){perror("syscall clock_gettime");return1;}printf("libc=%lld.%09ld, syscall=%lld.%09ld\n",(longlong)a.tv_sec, a.tv_nsec,(longlong)b.tv_sec, b.tv_nsec);return0;}
Compile and run strace -e trace=clock_gettime ./clock-paths. The second call explicitly enters the kernel; the first call may not produce this syscall record if it takes the available vDSO path. Both calls read the same monotonic clock; the latter should not be earlier than the former, but clock resolution allows the two results to be equal; scheduling may also increase the interval, so you cannot use a very small fixed difference as a mandatory pass condition. No syscall record does not mean the function did not execute. clock_gettime(3)
Wrappers Also Include Historical Compatibility
glibc can implement open using openat; time interfaces on 32-bit platforms may require time64 compatibility branches. Source code function names, dynamic symbol names, and kernel syscall names do not necessarily correspond one-to-one; seccomp policies and trace filter conditions should be determined based on the actual call paths of the target environment. open(2)
glibc removed the PID cache for getpid in version 2.25; you can no longer use "current getpid always returns directly from the cache" to explain benchmark results. getpid(2)
musl also requires architecture argument conventions, thread-local errno, cancellation points, and applicable vDSO paths. When comparing two libc implementations, select the version, target architecture, and specific functions, and check build artifacts and behavior; deriving performance from "five lines of code per syscall versus fifty lines" has no reliable significance.
How to Measure Wrapper Cost
First, clarify whether the measurement object is raw entry/exit, public API calls, or end-to-end operations including scheduling and device waits. Record kernel, libc, CPU, compilation options, and virtualization environment, and distinguish batch loop overhead from the target call.
strace is suitable for confirming paths and errors; tracing itself significantly perturbs latency and is not suitable for directly representing micro-benchmarks of untraced programs. For clock APIs, confirm whether vDSO is used; for futex, you must specify the wait value, whether it actually sleeps, and who wakes it up; you cannot treat "uncontested FUTEX_WAIT" as a fixed-cost operation.
Only consider bypassing the public interface when measurement shows that the wrapper cost is significant and you can clearly assume the semantics of cancellation and compatibility. Reducing unnecessary system calls and merging I/O is usually more worth checking first than copying architecture-specific assembly.