In the first compilation step, preprocessing (cpp) organizes our .c file into a .i file and among other tasks, includes headers in this file.
That is what logically happens as the first step, yes. In the dawn of time there was a separate program cpp, which performed this step.
But currently this step is logical-only, no production compiler actually generates the .i file, unless you ask it to. Instead, preprocessing feeds directly into the later stages of the compiler, producing assembly (.s) output. This arrangement is called "integrated preprocessor".
Similarly, the next step: .s -> .o transformation is bypassed by some compilers (this is called "integrated assembler"), while other compilers do invoke the as (assembler) to perform this step as a separate process.
My understanding is that cpp only includes library declarations in the .i file
It includes whatever is contained in the header file. It could be declarations, but it could also be definitions of functions and/or variables.
whereas the linker (ld) places the entire code of the stdio.h library in the a.out file.
On most UNIX systems there is no such thing as "stdio.h library". Rather stdio.h is provided by the system libc (of which there are several implementations).
In addition, "places entire code of the library in the a.out" is incorrect in several aspects:
- only the referenced parts of the library are included (not the entire library) in the case of using archive
libc.a
- something else entirely happens when linking with
libc.so (which is the default).
P.S. You can actually see what happens for yourself, there is no need to guess.
Create a trivial libfoo.a library, reference the header for that library in main.c, use gcc -E main.c > main.i to observe what ends up in the .i file, and objdump -d a.out to observe what happens with the code in libfoo.a during linking.
Extend this exercise to put the same code into libfoo.so, link with it, and observe how the picture changes.