问题
I have difficulty to understand the correct sequence of events. When a program written in abstract language is compiled, it is translated into machine code. Subsequently, only after the program has run, it is loaded into the ram, in the code segment. At this point, each instruction in the program will be on a specific memory address. When a function is called in assembly, the Call statement is typically followed by a label. I assume this label will be replaced with the function's memory address by the compiler. And this is where I absolutely can't understand. If the instructions are loaded into memory only when the program is running, thus obtaining each instruction its own memory address, how does the compiler know the memory address to which the label corresponds? If the function is not yet in memory, how can the program, compiled in binary code where the labels are nomore available, know the memory address, corrisponding to that label, where the function will be loaded at the moment of execution? I am a bit confused. Help me.
回答1:
A program contains several "sections" (some are optional):
- a section that holds code, usually referred to as the Text section
- a section that holds initial values for mutable global data
- a section that holds immutable constants, usually called rodata
- a section that has a set of relocation records
A section is stored as a contiguous chunk or block of memory in the program file on disc.
The loader creates memory chunks and loads the code, data, rodata into those; a stack will have been created, depending on the os, either by the loader, but also possibly by the forking of the parent process that creates the child process.
Knowing the final addresses, the loader also processes the relocation records. These relocations describe where in the text and data sections updates are needed for the final addresses of the sections loaded into memory.
The relocation mechanism is general purpose, as: code can refer to code, code can refer to data, data can refer to code, and data can refer to data.
A single relocation record describes a reference that needs to be updated. Each record describes:
- a referring source — at what offset in the text or data section to make an address update
- a referring target — which section is being referred to: code or data
what kind of update to make (some architectures have complex instruction encodings)
Some updates are for ordinary pointers, while others are for instructions. Instruction set architectures that have complex instruction offset/immediate encodings, like MIPS, RISC V, HP-PA, need to inform of the immediate encoding method.
Usually the referrer already has an offset, so the update is a matter of addition/summation of the base of the section being referred to, to the offset already in place at the referring source.
Other metadata in the program describes where to start, e.g. the initial the program counter, which would be as an offset into the text section.
Most processors today support (as fuz describes) position independent code (PIC). This is typically done via pc-relative addressing. The processor performs branches and calls within the single text section using pc-relative addressing modes, and thus, no relocation records are required for these instructions.
Dynamically loaded libraries add complexity since each DLL, and the main program to run, each have the format of a program, i.e. they each will have their own sections; each has its own text section. The relocations will also be capable of describing references to symbol imports, supported by additional sections holding symbol names, imports, and exports.
Object files (compiler output, pre-linking) typically follow this format as well. A single object file has these sections, with relocation records, symbol names, imports, exports. The linker's job is to merge object files into a single program or larger object file. During merge the linker resolves some relocations, but it cannot necessarily resolve all of them, so some may remain for the os loader to resolve.
Let's imagine that, on a system using PIC, there is a reference: a call (code-to-code), from one object file to another, and that the linker merges these object files. There will be a relocation record in the caller that refers to an imported symbol name (and in the other object file, an export of a symbol defined as some offset with its text section). Once the two object files' sections are merged (e.g. by simply concatenating them into one larger text section), the call there is now an intra-section reference, and the linker can compute the delta between the addresses of the caller and callee, and these will not change by future linking or loading. The linker will adjust the offset/immediate in the call instruction with that delta, and, knowing this reference is now resolved, omits this relocation record in the merge.
For reference, see:
- ELF (Executable and Linkable Format)
- COFF (Common Object File Format)
- Windows Portable Executable Format
- Relocation
- Object file
- Executable
- Position Independent Code
- Addressing Mode
回答2:
TL:DR: the distance from a call
to its target is a link-time constant.
The .o
object files you get from assembling asm have relocation records for symbols that aren't defined in that file.
When you link these .o
files into an executable or library, the linker lays out the .text
section from each .o
into one big .text
section for the executable and calculates the relative distance for each call
to reach its target. It encodes that relative displacement right into the machine code for each call
.
At run time no further relocations are needed: wherever the whole executable is loaded in memory, distances between instructions don't change. So no runtime relocations are ever needed for relative calls.
Related: Why are global variables in x86-64 accessed relative to the instruction pointer?
来源:https://stackoverflow.com/questions/59437093/function-call-labels-into-memory-addresses