I always thought that functions like printf()
are, in the last step, defined using inline assembly. That deep in the bowels of stdio.h is buried some asm code t
First, you have to understand the concept of rings.
A kernel runs in ring 0, meaning it has a full access to memory and opcodes.
A program runs usually in ring 3. It has a limited access to memory, and cannot use all the opcodes.
So when a software need more privileges (for opening a file, writing to a file, allocating memory, etc), it needs to asks the kernel.
This can be done in many ways. Software interrupts, SYSENTER, etc.
Let's take the example of software interrupts, with the printf() function:
1 - Your software calls printf().
2 - printf() processes your string, and args, and then needs to execute a kernel function, as writing to a file can't be done in ring 3.
3 - printf() generates a software interrupt, placing in a register the number of a kernel function (in that case, the write() function).
4 - The software execution is interrupted, and the instruction pointer moves to the kernel code. So we are now in ring 0, in a kernel function.
5 - The kernel process the request, writing to the file (stdout is a file descriptor).
6 - When done, the kernel returns to the software's code, using the iret instruction.
7 - The software's code continues.
So functions of the C standard library can be implemented in C. All it has to do is to know how to call the kernel when it need more privileges.
The standard library functions are implemented on an underlying platform library (e.g. UNIX API) and/or by direct system calls (that are still C functions). The system calls are (on platforms that I know of) internally implemented by a call to a function with inline asm that puts a system call number and parameters in CPU registers and triggers an interrupt that the kernel then processes.
There are also other ways of communicating with hardware besides syscalls, but these are usually unavailable or rather limited when running under a modern operating system, or at least enabling them requires some syscalls. A device may be memory mapped, so that writes to certain memory addresses (via regular pointers) control the device. I/O ports are also often used and depending the architecture these are accessed by special CPU opcodes or they, too, may be memory mapped to specific addresses.
The compiler generates the assembly from the C/C++ source code.
In general, library function are precompiled and distribute ad object. Inline assembler is used only in particular situation for performance reasons, but it's the exception, not the rule. Actually, printf doesn't seems to me a good candidate to be inline-assembled. Insetad, functions like memcpy, or memcmp. Very low-level functions may be compiled by a native assembler (masm? gnu asm?), and distribute as object in a library.
Well, all C++ statements except the semicolon and comments end up becoming machine code that tells CPU what to do. You can write your own printf function without resorting to assembly. The only operations that must be written in assembly are input and output from ports, and things that enable and disable interrupts.
However, assembly is still used in system level programming for performance reasons. Even though inline assembly is not supported, there is nothing that prevents you from writing a separate module in assembly and linking it to your application.
In Linux, strace
utility allows you to see what system calls are made by a program. So, taking a program like this
int main(){ printf("x"); return 0; }
Say, you compile it as printx
, then strace printx
gives
execve("./printx", ["./printx"], [/* 49 vars */]) = 0 brk(0) = 0xb66000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=119796, ...}) = 0 mmap(NULL, 119796, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa6dc0c7000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1811128, ...}) = 0 mmap(NULL, 3925208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa6dbb06000 mprotect(0x7fa6dbcbb000, 2093056, PROT_NONE) = 0 mmap(0x7fa6dbeba000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b4000) = 0x7fa6dbeba000 mmap(0x7fa6dbec0000, 17624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa6dbec0000 close(3) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c6000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c5000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c4000 arch_prctl(ARCH_SET_FS, 0x7fa6dc0c5700) = 0 mprotect(0x7fa6dbeba000, 16384, PROT_READ) = 0 mprotect(0x600000, 4096, PROT_READ) = 0 mprotect(0x7fa6dc0e7000, 4096, PROT_READ) = 0 munmap(0x7fa6dc0c7000, 119796) = 0 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e4000 write(1, "x", 1x) = 1 exit_group(0) = ?
The rubber meets the road (sort off, see below) in the next to last call of the trace: write(1,"x",1x)
. At this point the control passes from user-land printx
to the Linux kernel which handles the rest. write()
is a wrapper function declared in unistd.h
extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;
Most system calls are wrapped in this way. The wrapper function, as its name suggests, is little more than a thin code layer that places the arguments in the correct registers and then executes a software interrupt 0x80. The kernel traps the interrupt and the rest is history. Or at least that's the way it used to work. Apparently, the overhead of interrupt trapping was quite high and, as an earlier post pointed out, modern CPU architectures introduced sysenter
assembly instruction, which accomplishes the same result at speed. This page System Calls has quite a nice summary of how system calls work.
I feel that you will probably be a bit disappointed with this answer, as was I. Clearly, in some sense, this is a false bottom as there are still quite a few things that have to happen between the call to write()
and the point at which the graphics card frame buffer is actually modified to make the letter "x" appear on your screen. Zooming in on the point of contact (to stay with the "rubber against the road" analogy) by diving into the kernel is sure to be educational if a time consuming endeavor. I am guessing you would have to travel through several layers of abstraction like buffered output streams, character devices, etc. Be sure to post the results should you decide to follow up on this:)