Relocations reference symbols by index, not by name. Symbol 23 is the thing relocation 0 wants the address of. So the symbol table is the vocabulary every relocation in the file is written in, and a reader that mis-parses it produces relocations pointing at the wrong thing.
It is also the only place in a COFF object that records a section's final size in memory, because the section header's memory fields are all zero. That is a strange responsibility for a symbol table, and it is why the section-definition records inside it matter more than they look.
Eighteen bytes per record, six fields, and one of them is a union with the string table:
Name (8 bytes) — inline, or a string-table offset. Resolved in the previous concept.Value (4 bytes) — the symbol's offset within its section. This is the one address a compiler can know, because the section is its own private space for now.SectionNumber (2 bytes) — 1-based index into the section table. 0 means undefined; 0xFFFE means absolute.Type (2 bytes) — mostly 0, with 0x20 for a function and 0x0 for data.StorageClass (1 byte) — the field that actually carries meaning. In practice almost everything is 2 (external) or 3 (static).NumberOfAuxSymbols (1 byte) — how many extra 18-byte slots follow.That last field is the one that catches people. NumberOfSymbols counts slots, and a symbol with one aux record takes two. A reader that advances by one record per symbol desynchronises at the first aux record and never recovers.
Our file has NumberOfSymbols = 31 slots at 0x348. The first three records, raw:
00000348: 2e74 6578 7400 0000 0000 0000 0100 0000 .text........... 00000358: 0301 6c00 0000 0300 0000 bcbb d609 0100 ..l............. 00000368: 0000 0000 2e64 6174 6100 0000 0000 0000 .....data....... 00000378: 0200 0000 0301 3000 0000 0200 0000 27a4 ......0.......'. 00000388: dad5 0200 0000 0000 ........
Read the rows as three 18-byte records. Slot 0 is at 0x348, slot 1 at 0x35A, slot 2 at 0x36C:
| Slot | At | Name | Value | Section | Type | Storage | Aux | |
|---|---|---|---|---|---|---|---|---|
| 0 | 0x348 | .text | 0 | 1 | 0 | 3 STATIC | 1 | |
| 1 | 0x35A | the aux record — not a symbol | 0 | |||||
| 2 | 0x36C | .data | 0 | 2 | 0 | 3 STATIC | 1 | |
Slot 0 field by field, from the bytes:
| Offset | Bytes | Field | Value |
|---|---|---|---|
| +0 | 2e 74 65 78 74 00 00 00 | Name | .text, inline |
| +8 | 00 00 00 00 | Value | 0 |
| +12 | 01 00 | SectionNumber | 1 |
| +14 | 00 00 | Type | 0 |
| +16 | 03 | StorageClass | 3 = STATIC |
| +17 | 01 | NumberOfAuxSymbols | 1 — one record follows |
Which is llvm-readobj's: Name: .text, Value: 0, Section: .text (1), StorageClass: Static (0x3), AuxSymbolCount: 1. And the symbol is not at 0x350 — the next real symbol, .data, is at 0x36C. The 18 bytes in between are its aux record. This is the arithmetic that trips readers up: slots 0 and 1 are .text and its aux; slot 2 is .data.
The section-definition aux record is 18 bytes, and its field order is not the one the specification documents. Here are the bytes for .text:
6c 00 00 00 | 03 00 00 00 | bc bb d6 09 | 01 00 | 00 | 00 | 00 00 Length Relocations CheckSum Number Sel rsvd Lines
Read in the order the specification gives — Length, NumberOfRelocations, NumberOfLinenumbers, CheckSum, Number, Selection — and you get NumberOfLinenumbers = 0x09D6BBBC = 165,067,708. Absurd. Read it the way the compiler actually emits it, and every field is sensible:
| Offset | Size | Field | .text | llvm-readobj |
|---|---|---|---|---|
| 0x00 | 4 | Length | 108 | Length: 108 |
| 0x04 | 4 | NumberOfRelocations | 3 | RelocationCount: 3 |
| 0x08 | 4 | CheckSum | 0x09D6BBBC | Checksum: 0x9D6BBBC |
| 0x0c | 2 | Number | 1 | Number: 1 |
| 0x0e | 1 | Selection | 0 | Selection: 0x0 |
| 0x10 | 2 | NumberOfLinenumbers | 0 | LineNumberCount: 0 |
There is independent proof of this ordering that does not depend on believing any tool. The Number field at 0x0c is defined as the section's 1-based index. Check it against the section number in the owning symbol, for every aux record in every sample:
| Symbol's section | Number at 0x0c | Match |
|---|---|---|
1 (.text) | 1 | yes |
2 (.data) | 2 | yes |
3 (.bss) | 3 | yes |
4 (.xdata) | 4 | yes |
5 (.rdata) | 5 | yes |
6 (.rdata, COMDAT) | 6 | yes |
All 58 aux records across the five sample objects satisfy that invariant. A layout that placed NumberOfLinenumbers at 0x08 could not produce this coincidence 58 times running. This is the strongest form of evidence available when a specification and a compiler disagree: find a field whose value is constrained by something else in the file, and check it.
And note the field this settles the earlier question: .bss's aux record says Length = 4, even though its section header says VirtualSize = 0 and SizeOfRawData = 4. The size .bss will occupy in the linked image is recorded here and nowhere else in the object.
Beyond the section symbols, the ones that matter to relocations. These are the indices relocation records actually reference:
| Slot | Name | Value | Section | Storage |
|---|---|---|---|---|
| 0 | .text | 0 | 1 | STATIC |
| 2 | .data | 0 | 2 | STATIC |
| 4 | .bss | 0 | 3 | STATIC |
| 6 | .xdata | 0 | 4 | STATIC |
| 8 | .rdata | 0 | 5 | STATIC |
| 10 | .rdata (COMDAT) | 0 | 6 | STATIC |
| 12 | ??_C@_03PLHFFLIH@ptr?$AA@ | 0 | 6 | EXTERNAL |
| 15 | .pdata | 0 | 8 | STATIC |
And the named symbols further down, which is where Value starts to mean something:
| Name | Value | Section | Type | Storage |
|---|---|---|---|---|
add | 0 | 1 .text | 0x0020 function | EXTERNAL |
call_through | 32 | 1 .text | 0x0020 function | EXTERNAL |
use | 80 | 1 .text | 0x0020 function | EXTERNAL |
g_data | 0 | 2 .data | 0 | EXTERNAL |
g_addr | 16 | 2 .data | 0 | EXTERNAL |
table | 32 | 2 .data | 0 | EXTERNAL |
g_str | 0 | 5 .rdata | 0 | EXTERNAL |
use at 80 and table at 32 are the two numbers worth pausing on, because they are the whole "known offset, unknown address" idea made concrete. use is 80 bytes into .text — the compiler knows that exactly, because it laid the section out itself. What it cannot know is what .text's address will be after the linker concatenates four object files. So the symbol says "80 bytes into section 1", and the relocation says "add the final address of section 1 to this".
call_through at 32 and use at 80 are 48 bytes apart, which is the compiled size of call_through — you can measure it directly. And add at 0 through 32 is the other half of the 108-byte .text: 32 for add, 48 for call_through, 20 for use, 8 of padding. The symbol table is a map of the section, and the numbers fit.
Also note s_data is absent from the named list. It is static, so it is emitted as a STATIC symbol at the end of the table, and it has no linker-visible name. That is the difference between storage class 2 and 3 doing real work: EXTERNAL symbols are the ones other object files can refer to, STATIC ones are private to this file.
$ clang --target=x86_64-pc-windows-msvc -c sample.c -o sample_msvc.obj
$ llvm-readobj --symbols sample_msvc.obj | head -40
$ xxd -s 0x348 -l 72 sample_msvc.objThe exercise worth doing is the one that proves the aux record ordering, because it is the kind of check that catches a wrong assumption permanently:
AuxSymbolCount: 1 and StorageClass: Static, note its Section: .name (N) and its aux Number:. They must be equal. Ours are, for all nine section symbols.VirtualSize against the aux Length. For .bss the header says 0 and the aux says 4. The aux is the real number.NumberOfSymbols is 31 but there are far fewer names, because of the aux records.And to see the three dialect differences in the same table:
$ llvm-readobj --symbols sample_m32.obj | grep -A2 'Name: add'
Name: _add # i386: leading underscore
$ llvm-readobj --symbols sample_msvc.obj | grep -A2 'Name: add'
Name: add # x64: no underscoreWithout looking back: you are walking the symbol table and reach slot 1. What is there, and what mistake have you made if you tried to read it as a symbol?
Your linker is asked for the size of .bss in the object it is about to place. It reads the section header, finds VirtualSize = 0, and allocates nothing. The program then corrupts memory at startup. What is the correct source for that size, and why is the section header actively misleading here?
The general habit: a field that is zero because of a container-level invariant is not the same as a field that is zero because the thing is empty. Before trusting a size, find out whether the zero is meaningful or merely structural.
A symbol's Value is an offset within its section, and the section table is what gives that section meaning. That is the same two-step indirection as a DWARF DW_AT_type reference, and the same lesson: the DWARF file tables resolve a number to a name through a table, and COFF resolves an address to a symbol through one.
The COFF storage classes are the direct ancestor of the ELF ones you already know. STATIC here is STB_LOCAL in ELF, EXTERNAL is STB_GLOBAL, and the ELF visibility concept covers what happens when they meet at link time.
Now the table that consumes all of this. A symbol is a name and a place; a relocation is a question about a byte.
Next: Relocations — ten bytes each, and a second place where the specification and the compiler disagree.