76

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 that actually tells CPU what to do. For example, in dos, I remember it was implemented by first moving the beginning of the string to some memory location or register and than calling an intterupt.

However, since the x64 version of Visual Studio doesn't support inline assembler at all, it made me wonder how there could be no assembler-defined functions at all in C/C++. How does a library function like printf() get implemented in C/C++ without using assembler code? What actually executes the right software interrupt? Thanks.

edtheprogrammerguy
  • 5,957
  • 6
  • 28
  • 47
Jack
  • 761
  • 1
  • 7
  • 4
  • 22
    Hard to know where to begin, as everything you think you know is wrong. You need to read a few wikipedia articles on compiling and linking. You might also want to take a look at the source of stdio.h (it's just text), in which you won't find any assembler code for any C++ implementation. –  Mar 14 '10 at 17:11
  • 4
    Visual Studio x64 doesn't support *inline* assembler. That doesn't mean you can't have assembler code. You can still have assembler, just not inline. Tronic's answer below is correct. You should also look into compiler intrinsics. –  Mar 14 '10 at 17:19
  • @Neil Butterworth: No, not everything I know is wrong. It may look like it from my question, but of course I read maybe to many articles on compiling and linking. It is hard to express and question in such a different language from your native language. Of course I know everything C compiler produces is assembler/optcodes. I just thought that C has no "statement" to produce software interrupt and so. – Jack Mar 14 '10 at 17:41
  • 1
    @Jack I didn't mean to denigrate your language skills (in fact your question is very well expressed as far as English usage goes), but only to point out that your idea that the code was somehow in stdio.h was wrong. I see now that was perhaps not what you meant. –  Mar 14 '10 at 17:49
  • @Neil Butterworth: Yeah, I know I expressed it bad. I just wanted you to know that I know what compiler and linker does. Well, I just miss one morw thing to know what I wanted to. So, if I am right now, is header files there is only declarations of functions so that compiler let us call them, but the actual definition (code) is in separate obj files or libraries. So, last question, is the actual code in some C library, or is the actual code taken from some system library? So that linker acess some library that is offered by OS? – Jack Mar 14 '10 at 18:10
  • 1
    @Jack It's not a yes/no question. Some systems don't have an OS. The answer depends on your specific system/ In the case of Windows, both 32 and 64-bit, the user-level code works by calling the system linraries (which are just DLL libraries you could write). At some point, deep within the hierarchy of calls, some code that could not be expressed in C is executed. How that code got generated is not too interesting, but typically it will have been written in assembler. Whether that was inline or straight ASM is not important –  Mar 14 '10 at 18:23
  • OK, I think I finally understand it - at least a little bit. So, on lets say windows, C/C++ calls windows libraries. So, stdio says the format of its functions, and they code is actually linked from system library. But, what is the syntax to say to C compiler that this code can be found in this file or library? And, why is there actual stdio.h , if I call system functions? – Jack Mar 14 '10 at 19:28
  • @Jack The compiler knows nothing about the libraries - that's what the LINKER does. The header files, like stdio.h say to the compiler "these functions will be dealt with by the linker", don't worry about them. As I said originally, you need to read up on compilers and linkers. And so this is my last post in this thread. –  Mar 14 '10 at 19:41
  • 7
    Everything you know isn't wrong. But in the era of open source, all it takes to answer such a question for yourself is curiosity plus time. To show you that it's possible, my answer starts digging from the prototype for *printf* and doesn't skip any steps until you reach *syscall*...with links to the actual source files in their repositories. It took a long time to write, I hope it helps. :) – HostileFork says dont trust SE Mar 15 '10 at 05:08
  • 1
    Yes, it helps. I just wanted direction to go. My problem is that I don´t have informations I want. I collect pieces of infos from many sources over the internet, but tha 99% of time there is just "printf() is declared in stdio.h" with no further explanantion. I will digg deeper. – Jack Mar 15 '10 at 13:04
  • 1
    While there has been contention over whether the content of this post can be published here vs. not, I will be clear. It's not legal to post it on StackExchange unless they abide by the license [CC-BY-SA-NC-4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) At one point someone pasted the answer here and I let it slide. But that was then and this is now. This content may only be linked, it may not be reproduced unless the NC aspect is preserved. http://blog.hostilefork.com/where-printf-rubber-meets-road/ – HostileFork says dont trust SE Mar 15 '10 at 00:58

6 Answers6

22

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.

Macmade
  • 52,708
  • 13
  • 106
  • 123
  • 11
    printf() works on systems that don't have a kernel, or a ring-based architecture –  Mar 14 '10 at 17:37
  • x86's ring 3 and ring 0 work exactly like user / kernel mode on architectures that only provide 2 privilege levels (i.e. most non-x86 CPUs that run Unix or Linux). With no kernel, it's really more like your freestanding program *is* the kernel, or at least runs with full privileges so `printf` is just a function inside the kernel. (Like the Linux kernel's `printk`.) – Peter Cordes Jul 07 '18 at 21:01
  • @PeterCordes Yeah. It's like attaching an LCD to a microcontroller and programming the microcontroller to display some strings on it. – Sandeep May 16 '21 at 22:28
6

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:)

Daniel Genin
  • 463
  • 5
  • 10
  • It seems the information on the linked web page describing system calls in Linux is outdated. In particular vsyscall page cannot be found using the provided example code on kernels newer than 2.6 and possibly some earlier ones too. – Daniel Genin Nov 30 '12 at 17:28
  • More specifically due to address space randomization the vsyscall page is no longer mapped at a fixed address. The address of the page can still be obtained by looking up the ELF auxv AT_SYSINFO parameter (http://articles.manugarg.com/aboutelfauxiliaryvectors.html). – Daniel Genin Nov 30 '12 at 19:01
5

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.

Tronic
  • 10,250
  • 2
  • 41
  • 53
  • But these calls are not deep within stdio.h –  Mar 14 '10 at 17:22
  • Added info about direct hardware access. – Tronic Mar 14 '10 at 17:47
  • 3
    All correct but just FYI amd for others posting in this thread most modern OS and architectures now use special opcodes to actually execute systemcalls(e.g sysenter and sysexit on x86), rather than using software interrupts, to improve performance. – PinkyNoBrain Mar 14 '10 at 18:34
1

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.

Vlad
  • 9,180
  • 5
  • 48
  • 67
  • 1
    You can't make a system call without assembly, or calling a library function that was written in assembly. C compilers don't have builtins / intrinsics for setting up args in registers and running x86 `syscall` / `sysenter` or `int`, so this is done with hand-written asm. – Peter Cordes Jul 07 '18 at 21:07
0

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.

Giuseppe Guerrini
  • 4,274
  • 17
  • 32
-7

The compiler generates the assembly from the C/C++ source code.

Terry Mahaffey
  • 11,775
  • 1
  • 35
  • 44
  • There is hand-written or inline asm at some point to invoke the underlying system call. I'm not aware of a compiler with a builtin or intrinsic for x86's `syscall`, `sysenter`, or `int` instructions. Of course this isn't in `stdio.h`, it's in the already-compiled libc – Peter Cordes Jul 07 '18 at 21:04