Every byte of code and data in the object belongs to a section, and the section header is the only thing that says where those bytes are, how many there are, and what the linker is supposed to do with them. Forty bytes per section, ten fields, and two of them are the reason a naive reader produces nonsense.
The trap: COFF section headers carry both a "size in the file" field and a "size in memory" field, and in a relocatable object the memory one is always zero. A reader that expects the memory field to be meaningful — because in a PE image it is — will report every section as empty.
Group the ten fields by the question each answers:
Name — eight bytes, and there is an escape hatch for longer names.SizeOfRawData and PointerToRawData. A file offset and a length.VirtualSize and VirtualAddress. Both zero here, and that is not a bug.PointerToRelocations, NumberOfRelocations, PointerToLinenumbers, NumberOfLinenumbers.Characteristics, which is the next concept.The .text section header, at file offset 0x14, all forty bytes:
| Offset | Size | Field | Bytes | Value |
|---|---|---|---|---|
| +0 | 8 | Name | 2e 74 65 78 74 00 00 00 | .text |
| +8 | 4 | VirtualSize | 00 00 00 00 | 0 |
| +12 | 4 | VirtualAddress | 00 00 00 00 | 0 |
| +16 | 4 | SizeOfRawData | 6c 00 00 00 | 108 |
| +20 | 4 | PointerToRawData | 7c 01 00 00 | 0x17C |
| +24 | 4 | PointerToRelocations | e8 01 00 00 | 0x1E8 |
| +28 | 4 | PointerToLinenumbers | 00 00 00 00 | 0 |
| +32 | 2 | NumberOfRelocations | 03 00 | 3 |
| +34 | 2 | NumberOfLinenumbers | 00 00 | 0 |
| +36 | 4 | Characteristics | 20 00 50 60 | 0x60500020 |
Every value was decoded from the raw bytes and then required to match llvm-readobj --sections, which reports VirtualSize: 0x0, VirtualAddress: 0x0, RawDataSize: 108, PointerToRawData: 0x17C, PointerToRelocations: 0x1E8, RelocationCount: 3, LineNumberCount: 0 and Characteristics [ (0x60500020).
Now the section that makes the two-size distinction unavoidable. Here are .text, .data and .bss side by side, from llvm-readobj:
| Section | SizeOfRawData | PointerToRawData | VirtualSize | Bytes in the file? |
|---|---|---|---|---|
.text | 108 | 0x17C | 0 | yes |
.data | 48 | 0x206 | 0 | yes |
.bss | 4 | 0x0 | 0 | no |
.bss claims four bytes, and its raw data pointer is zero. There is nothing to point at, because there is nothing there: the four bytes are uninitialised global storage that the linker will allocate and zero. The section header is describing a region that will exist in the output image and does not exist in this file.
And note what .bss's section header does not tell you: how big the region will finally be. SizeOfRawData = 4 is a file fact. The memory size is recorded elsewhere — in the symbol table's aux record, where .bss's Length is 4. That is the field to read, and it is the subject of the symbol table concept.
Name is eight bytes, which is not enough for .llvm_addrsig or any real mangled C++ symbol. The escape hatch: if the first byte is / (0x2f), the remaining seven bytes hold a decimal offset into the string table. Our section 9 stores exactly that:
00000154: 2f31 3700 0000 00 /17.....
That is /17 — slash, then the ASCII digits one and seven. Offset 17 in the string table is the string .llvm_addrsig. llvm-readobj confirms it: Name: .llvm_addrsig. Module 2 opens the string table properly; the mechanism is all you need here.
Three section headers, straight out of the file, 0x14 through 0x8B:
00000014: 2e74 6578 7400 0000 0000 0000 0000 0000 .text........... 0000001c: 0000 0000 6c00 0000 7c01 0000 e801 0000 ....l...|....... 0000002c: 0000 0000 0300 0000 2000 5060 ........ .P` 0000003c: 2e64 6174 6100 0000 0000 0000 0000 0000 .data........... 0000004c: 3000 0000 0602 0000 3602 0000 0000 0000 0.......6....... 0000005c: 0200 0000 4000 50c0 ....@.P. 00000064: 2e62 7373 0000 0000 0000 0000 0000 0000 .bss............ 0000006c: 0000 0000 0400 0000 0000 0000 0000 0000 ................ 0000007c: 0000 0000 0000 0000 8000 30c0 ..............0.
Read .bss field by field, because it is the instructive one:
2e 62 73 73 00 00 00 00 — .bss, padded with zeros to eight bytes.VirtualSize 0 and VirtualAddress 0, as everywhere.04 00 00 00 — SizeOfRawData = 4. The section is four bytes wide.00 00 00 00 — PointerToRawData = 0. There is no raw data. A reader that computes PointerToRawData + SizeOfRawData and seeks there lands on the wrong part of the file; a reader that trusts the pointer will happily read four bytes belonging to something else.80 00 30 c0 — Characteristics = 0xC0300080.And .text, for contrast: offset 0x24 is 6c 00 00 00 = 108 bytes, offset 0x28 is 7c 01 00 00 = 0x17C, and the first eight bytes at 0x17C are:
0000017c: 50 89 54 24 04 89 0c 24 P.T$...
50 is push rax, then 89 54 24 04 is mov [rsp+4], rdx. That is not your code — it is the x64 Windows calling convention storing the second argument into the caller's shadow space. It is the first thing the compiler emits for any function, and it is why a COFF object is not a portable artifact: the bytes are for one CPU and one ABI.
$ clang --target=x86_64-pc-windows-msvc -c sample.c -o sample_msvc.obj
$ llvm-readobj --sections sample_msvc.obj | head -30
$ llvm-objdump -h sample_msvc.objThree checks worth making on every file you open:
VirtualSize zero everywhere? If some are non-zero you are not looking at an object, or you have mis-parsed. In a PE image the same fields carry real values.PointerToRawData zero while SizeOfRawData is not? That is .bss, and it is legal. It means the bytes are zeros the linker will allocate.SizeOfRawData ever end in 0, 8, 10 or 16 rather than 0 mod 4? It can — the .rdata sections here are 5 and 4 bytes — because an object stores exact bytes, not the padded size a linked image needs.To see the same fields with real values, compare against an image. The PE section table has the identical 40-byte structure with non-zero VirtualAddress and VirtualSize, and that difference is the entire content of this concept.
Without looking back: a section header reports SizeOfRawData = 4 and PointerToRawData = 0x0. What does the linker do with this section, and where does it find out how big it will finally be?
Your tool lists the sections of a COFF object so it can show a user what is inside. On sample_msvc.obj it reports that .bss is 0 bytes, and the user says that is wrong because the C source clearly declares int g_bss;. What is the most likely cause, and what is the smallest correct fix?
The general habit: when a tool misreports one section, check whether it is reading a field whose meaning differs between the two variants of the format. Here one field is meaningful in a linked image and always zero in an object, and that is enough to break an entire listing.
This 40-byte structure is identical in COFF objects, PE images and DLLs — the PE course decodes the same ten fields with real values. The single difference that matters for this concept is that in an image, VirtualAddress and VirtualSize are the loadable truth, and in an object they are placeholders.
That leaves the last field of the header, and the one place in COFF where a careless bitmask produces a confident wrong answer: Characteristics. Alignment in particular is not a flag list.
Next: Section Characteristics — and why a value of 5 in the alignment field makes seven different alignment tests pass at once.