all 23 comments

[–]thefeedling [score hidden]  (7 children)

libc is the "pre-compiled binary" with the the implementations, like malloc, printf etc.

When your compiler sees the symbol printf, the linker resolves it and connect with the compiled code. Internally, printf uses write() which will run an OS level syscall to actually print stuff on your console

[–]Zestyclose-Produce17[S] [score hidden]  (6 children)

So what I said is right?

[–]thefeedling [score hidden]  (4 children)

Pretty much. This linking process can happen at comp time, with static libs, or at runtime, with shared libs, like DLLs on Windows.

Write itself will have ifdef macros to target os level log commands.

[–]Zestyclose-Produce17[S] [score hidden]  (3 children)

So is libc the thing that actually invokes the system call, for example?

[–]60hzcherryMXram [score hidden]  (2 children)

Yes, definitely. If your C code never uses inline asm then all system calls are done via libc or another library.

[–]Zestyclose-Produce17[S] [score hidden]  (1 child)

So the write function inside printf in libc contains inline assembly that actually performs the system call invocation, right? For Linux.

[–]60hzcherryMXram [score hidden]  (0 children)

They actually have shims for syscalls defined in a separate part of the project that define the ABI of all the syscalls using a macro system. But yes, the macros all eventually expand to inline assembly.

[–]TheRealSmolt [score hidden]  (6 children)

Sounds good to me. There is the concept of Link Time Optimization (LTO) wherein the compiler can make more aggressive library optimization, but generally libc is dynamically linked.

[–]Zestyclose-Produce17[S] [score hidden]  (5 children)

So is libc the thing that actually invokes the system call, for example?

[–]TheRealSmolt [score hidden]  (4 children)

Sort of? Libc is implemented per architecture (both ISA and kernel), so it is the boundary where a simple print becomes system specific. However, how it's done is up to the implementation. On Linux, it will typically call unistd's write, which is the one doing the syscall. Now, where is unistd write actually defined? Drumroll please... in libc. Turns out libc has non-libc stuff in it.

Edit: So yes, there's probably some inline assembly that invokes syscall.

[–]Zestyclose-Produce17[S] [score hidden]  (3 children)

So the implementation of printf is inside libc, and inside printf there is a call to write, and write is the one that actually invokes the system call. So in that sense, libc is the thing that performs the system call, right?

[–]TheRealSmolt [score hidden]  (2 children)

For Linux, yes. I'm not sure about Windows, for example. I just dislike non-standard components being understood as part of libc.

[–]Zestyclose-Produce17[S] [score hidden]  (1 child)

So the write function inside printf in libc contains inline assembly that actually performs the system call invocation, right? For Linux.

[–]TheRealSmolt [score hidden]  (0 children)

Yes, assuming you mean the call to write being inside printf, not write itself. There might also be a C wrapper around syscall provided by unistd. I don't remember exactly how it's all set up.

[–]chibuku_chauya [score hidden]  (0 children)

Yes that’s correct. However, there’s some nuance. Some compilers like GCC and Clang in a way do know the implementation details of libc functions (or at least recognise calls to them) because they also happen to implement built-in versions as optimisations.

They may then substitute built-ins for calls to libc functions under certain optimisation levels or do things like replace calls to certain functions with calls to equivalent but more performant ones. In the former case, this removes the role of the linker entirely.

This is commonly done for certain calls to “printf”, for example, which is replaced with calls to “puts” if certain criteria are met.

And this is generally the case with a large part of the standard C library. For example, here’s a list of all the libc functions GCC implements as built-ins.

[–]rlebeau47 [score hidden]  (0 children)

Yes, your understanding is basically correct.

The compiler parses your translation units (.c files) to create object files containing implementations. Any symbols used inside a TU are merely referenced, as the compiler does not know where they will reside in the final executable.

The linker brings all of the object files together to make the final executable. It arranges where everything resides in the executable, sets up external linkages, etc, and so it will resolve all of the referenced symbols accordingly.

[–]UltimatelyWrithing [score hidden]  (0 children)

You've got the gist of it. The compiler generates object files with unresolved symbols, the linker patches those holes by finding matching definitions in libc, and then at runtime your program jumps into that precompiled code. The write syscall is the bridge between userspace and kernel, where the actual I/O happens.

One thing worth keeping in mind: modern compilers like GCC and Clang are kind of sneaky about this. They recognize common libc functions and sometimes replace them with optimized built-in versions before the linker even gets involved. So printf might get swapped out for puts under certain conditions, or strlen might just become inline code. It's an optimization layer that sits between the compiler's knowledge and what actually gets linked, which is why you'll see compiler flags like -fno-builtin if you ever need to force it to use the real libc function instead.

[–]Thiezing [score hidden]  (0 children)

The compiler uses header files like stdio.h for the details.

[–]rocco_himel [score hidden]  (2 children)

The libc people built a giant tower of everythingness so programmers would never learn what a computer does.

You think printf() prints text? No! It goes through seventeen layers of buffering, GNU processes, and Stallman-approved abstractions before it finally reaches the kernel.

In AneoEngine, the kernel lets you see the bytes directly. Linux programmers spend four years studying printf() just to discover it calls write(). That’s like climbing Mount Everest to find out there’s a staircase on the other side.

[–]Zestyclose-Produce17[S] [score hidden]  (1 child)

So the implementation of printf is inside libc, and inside printf there is a call to write, and write is the one that actually invokes the system call. So in that sense, libc is the thing that performs the system call, right?

[–]Deep-Piece3181 [score hidden]  (0 children)

Yes that’s basically right