There is no such thing as a C application. Code written in C is compiled to a binary ELF file on Linux or a .exe file on Windows.
The answer to the question is it depends if the function requires a syscall or not. If it doesn't require a syscall (on Linux):
- You launch the ELF executable
- The ELF executable is interpreted and the code and data is placed in memory. A new process takes place. All code including the Linux kernel code that the process requires is placed into memory and linked against the user code (dynamic linking).
- When the process calls a function, its arguments are placed on the stack, the function code runs and the function returns.
It is as simple as that for a situation when the kernel function doesn't require a syscall.
If it requires a syscall:
- Steps 1 and 2 above.
- The function is called as usual with its arguments on the stack.
- When the kernel function requires to access hardware (like when using printf() to print to the screen), it uses system calls.
- A system call is done using software interrupt 0x80 (the assembly instruction int 0x80).
- The CPU is interrupted, looks in the IDT (https://wiki.osdev.org/Interrupt_Descriptor_Table), determines the address of the handler and jumps to the handler.
- The interrupt handler for a system call contains a special instruction: SYSENTER (https://wiki.osdev.org/Sysenter). The SYSENTER instruction makes the processor switch from user mode to kernel mode. A special MSR register contains the address at which the processor will jump when executing a SYSENTER instruction. To write to this register, the OS uses the WRMSR special instruction at boot.
- The CPU jumps to the address specified by the MSR register. It is now in kernel mode. It runs whatever code is needed to perform the requested operation (specified in EAX by the caller). It then returns the result to the caller by using certain registers depending on the system call. To return it calls SYSEXIT.
- The request was fulfilled, so the user code can continue where it was.
Here is an example of Hello World app in assembly which does a syscall. Your printf() function will be compiled to something similar.
section .data
hello: db 'Hello world!',10 ; 'Hello world!' plus a linefeed character
helloLen: equ $-hello ; Length of the 'Hello world!' string
section .text
global _start
_start:
mov eax,4 ; The system call for write (sys_write)
mov ebx,1 ; File descriptor 1 - standard output
mov ecx,hello ; Put the offset of hello in ecx
mov edx,helloLen ; length of the string in edx
int 80h ; Call the kernel
mov eax,1 ; The system call for exit (sys_exit)
mov ebx,0 ; Exit with return code of 0 (no error)
int 80h
The right answer would be
printf() -> User-space library -> system call -> kernel driver
The printf() function is placed in RAM alongside your code (and dynamically linked against it), the function is called, makes a system call which brings the processor in kernel mode using SYSENTER and then calls kernel drivers which do the required task like writing to the screen or writing to IO devices.