










Warning
If someone is reading this blog, please be aware that the writer DID NOT consider the experience of the other readers.
After all, the most important thing is about writing things down for better memorization.
We’ve already seen this before in class 1:
1 | .c --precompile--> .i --compile--> .s --assemble--> .o --link--> .out |
Today’s lesson would be focusing on the process where the object file(.o) being transformed into an executable(.out).
main.c, the compiler would not know which address to jump to unless we link the other file with main.c together.For example, if we have these three source files:
1 |
|
And then compile them separately, we would get three object files: a.o, b.o and main.o.
Then we will have to link them together to get the final expected executable file:
1 | gcc -static a.o b.o main.o |
Note: the flag -static is to explicitly tell gcc to use static linking, as modern versions of gcc would use dynamic linking by default.
The linking process would fail if we are missing one of the object files:
1 | gcc -static b.o main.o |
Without linking, the compiler would never know where the definitions of foo or x y are.
But How exactly does the compiler do this?
| ELF type | Explanation | Example(s) |
|---|---|---|
| Relocatable File | Object files that can be combined with other object files during linking to create an executable or shared object | .o, .obj |
| Executable File | Files that contain a program ready to be executed directly by the system | .exe, regular Linux programs |
| Shared Object File | Libraries that can be loaded and linked dynamically at runtim by multiple programs | .so, .dll |
| Core Dump File | Files containing a snapshot of a program’s memory when it crashes or terminates abnormally | core.1234 (1234 for the PID) |
| Section | Explanation |
|---|---|
| ELF_Header | Contains ident_num, version, machine type, entry point, etc. |
| .text section | Contains executable code/instructions of the program in machine code format |
| .rodata section | Contains read-only data like string literals, constants and static lookup tables |
| .data section | Contains initialized global and static variables that can be modified at runtime |
| .bss section | Contains uninitialized global and static variables (zeroed at program start) |
| … | … |
Note: modern compiler would put variables initialized with value 0 into .bss section rather than .data section by default
Note: We can use objdump -h to see those sections’ info.
When source files are compiled into object files separately, the name of the functions and variables declared in the file are stored as symbols.
Each object file has a symbol table containing:
During linking, the linker would scan all of the object files to match the defined and undefined symbols as much as possible.
In the example above, we have function foo and variable x & y defined in other source files other than main.c. It is the linker which put them together to form a complete executable file that include all of those symbols and their definitions.
Initially, each object files’s code is written assuming it starts at address 0.
The linker would:
Take the example above:
1 | objdump -d main.o |
Notice the zeros in the address references:
1 | 10: e8 00 00 00 00 call 15 <main+0x15> |
This is where the relocation placeholder is. This blank address will be filled in when linking happens.
Also, notice the instructions before call:
1 | 4: 8b 35 00 00 00 00 mov 0x0(%rip),%esi # a <main+0xa> |
These are actually calling to variable x and y. As you can see, they are also placeholders like the foo one mentioned above.
Inside main.o lies a table(Relocatable Section) indicating the symbols relocated from other elf files.
readelf -a main.o :
1 | Relocation section '.rela.text.startup.main' at offset 0x198 contains 3 entries: |
The reason why the address of foo needs to be substracted by 4 is that, the offset encoded in call instruction is calculated relative to the address of the next instruction, which is 4 bytes ahead of the current instruction on x86-64.
-4 adjustment is a platform-related design – pc always points to the next instruction in x86, which is likely intended to make shift for the poor hardware in the age of 1970s.-4 adjustment.Whimsy
Also, we can use nm to take a look at the symbol table stored inside main.o:
1 | nm main.o |
Note:
a.out compiled/linked with -static flag would contain symbols from glibc as static linkage would link glibc by default.
To get an a.out executable with a clean symbol table, one should remove the -static flag from the compilation command.
gcc does not only link those code/data we’ve written in that three source files. It also links a lot of other standard libraries to ensure the proper execution of the program.gcc a.o b.o main.o -Wl,--verbose and you’ll see that gcc uses ld to link a lot of files including a.o, b.o and main.o.ld a.o b.o main.o would output an a.out that would trigger a Segmentation Fault.Static linking would create a large executable file as it includes the entire code of the used libraries. When it comes to larger project which links a certain amount of libraries, the size of the executable may become significantly large, making it impractical for deployment or storage.
Dynamic linking was invented to resolve this issue: The system loads the requried shared libraries into memory(if not already loaded) when the executable is run and resolves the symbols.
Running executables compiled and linked with dynamic linking strategy is actually passing it to a dynamic linker.
We can use readelf -l a.out | grep interpreter to get the dynamic linker used for executing:
1 | readelf -l a.out | grep interpreter |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。