Back to course

Section Characteristics

17 minintermediateModule 1: The Relocatable ObjectContinues from coff-section-table
17 min · Module 1: The Relocatable Object · Bitfields

Why This Matters

The last four bytes of every section header is a 32-bit field that decides how the linker treats that section: is it code, is it writable, must it be executable, can it be thrown away. It is also the field most often mis-decoded, because part of it is a set of independent flags and part of it is a four-bit number that only looks like a flag.

Getting that wrong does not produce a crash. It produces a tool that reports a section as having seven different alignments at once, and an alignment that appears in no list of valid values.

A Simple Model

Split the 32 bits into three regions, because they obey three different rules:

  1. Bits 0–3 and scattered high bits — ordinary independent flags. Test each one, name it, move on.
  2. Bits 20–23 — a single four-bit field holding a number from 0 to 15. Exactly one alignment applies.
  3. Bits 31–28 — the memory permission bits, which happen to be three separate flags stacked at the top.

The mental model that keeps this straight: a value of 5 in a four-bit field is the binary number 0101, not "flag 0 and flag 2". Testing the field against fifteen named constants is the correct approach; testing each of those constants as a bitmask is not.

The Actual Detail

Take .text's real value, 0x60500020, and take it apart:

0x60500020  =  0110 0000 0101 0000 0000 0000 0010 0000
                 |           |           |           |
                 |           |           |           +-- bit 5:  CNT_CODE
                 |           |           +-- bits 20-23: 0101 = 5 = ALIGN_16BYTES
                 |           +-- bit 30: MEM_READ
                 +-- bit 29: MEM_EXECUTE
MaskNameTest
0x00000020IMAGE_SCN_CNT_CODEflag — test with &
0x00000040IMAGE_SCN_CNT_INITIALIZED_DATAflag
0x00000080IMAGE_SCN_CNT_UNINITIALIZED_DATAflag
0x00001000IMAGE_SCN_LNK_COMDATflag
0x00000800IMAGE_SCN_LNK_REMOVEflag
0x02000000IMAGE_SCN_MEM_DISCARDABLEflag
0x20000000IMAGE_SCN_MEM_EXECUTEflag
0x40000000IMAGE_SCN_MEM_READflag
0x80000000IMAGE_SCN_MEM_WRITEflag
0x00F00000IMAGE_SCN_ALIGN_*field — shift, then compare

And here is the trap, stated as the exact bug it produces. 0x60500020 has the value 5 in the alignment field. A decoder written as a list of bitmask tests does this:

ALIGN_1BYTES 0x00100000 0x60500020 & 0x00100000 = 0x00100000 -> reports 1-byte
ALIGN_2BYTES 0x00200000 0x60500020 & 0x00200000 = 0x00000000 -> no
ALIGN_4BYTES 0x00300000 0x60500020 & 0x00300000 = 0x00100000 -> reports 4-byte
ALIGN_8BYTES 0x00400000 0x60500020 & 0x00400000 = 0x00400000 -> reports 8-byte
ALIGN_16BYTES 0x00500000 0x60500020 & 0x00500000 = 0x00500000 -> reports 16-byte
ALIGN_32BYTES 0x00600000 0x60500020 & 0x00600000 = 0x00400000 -> reports 32-byte
ALIGN_64BYTES 0x00700000 0x60500020 & 0x00700000 = 0x00500000 -> reports 64-byte

Seven alignments reported, where the file has one. The correct decode is a shift and a compare against a lookup:

align = (Characteristics >> 20) & 0xF // = 5
// 5 = IMAGE_SCN_ALIGN_16BYTES. One answer.

This was not a hypothetical. It is the first thing that went wrong when the independent parser for this course was written, and the reason the parser now masks the field explicitly rather than relying on its own flag list.

Why is a field, and not a flag? Because alignment values are ordinal, not independent. 1-byte, 2-byte, 4-byte, 8-byte and 16-byte are increasing amounts of the same thing, so the format stores "how many steps up the ladder" rather than five separate yes/no bits. Four bits leaves room for 16 steps, which covers 1 byte up to 8 KB.

Every section in the reference object

SectionValuealignFlags llvm-readobj prints
.text0x605000205CNT_CODE, ALIGN_16BYTES, MEM_EXECUTE, MEM_READ
.data0xC05000405CNT_INITIALIZED_DATA, ALIGN_16BYTES, MEM_READ, MEM_WRITE
.bss0xC03000803CNT_UNINITIALIZED_DATA, ALIGN_4BYTES, MEM_READ, MEM_WRITE
.xdata0x403000403CNT_INITIALIZED_DATA, ALIGN_4BYTES, MEM_READ
.rdata0x401000401CNT_INITIALIZED_DATA, ALIGN_1BYTES, MEM_READ
.rdata (COMDAT)0x401010401CNT_INITIALIZED_DATA, LNK_COMDAT, ALIGN_1BYTES, MEM_READ
.debug$S0x423000403CNT_INITIALIZED_DATA, ALIGN_4BYTES, MEM_DISCARDABLE, MEM_READ
.pdata0x403000403CNT_INITIALIZED_DATA, ALIGN_4BYTES, MEM_READ
.llvm_addrsig0x001008001LNK_REMOVE, ALIGN_1BYTES

Read that table as the linker's instruction sheet. Code is executable and read-only. .data is writable. .bss is writable and uninitialised. .debug$S is discardable, which means the linker throws it away unless debug information is requested — the mechanism behind a debug build's size. .llvm_addrsig is marked remove because it is metadata for the linker, not content for the program.

A Real Example

Here are the characteristic fields as they actually sit in the file, at the last four bytes of each section header:

.text  hdr 0x14, chars at 0x38:  20 00 50 60   -> 0x60500020  align 5
.data  hdr 0x3c, chars at 0x60:  40 00 50 c0   -> 0xc0500040  align 5
.bss   hdr 0x64, chars at 0x88:  80 00 30 c0   -> 0xc0300080  align 3
.xdata hdr 0x8c, chars at 0xb0:  40 00 30 40   -> 0x40300040  align 3
.rdata hdr 0xb4, chars at 0xd8:  40 00 10 40   -> 0x40100040  align 1
.rdata hdr 0xdc, chars at 0x100: 40 00 10 40   -> 0x40101040  align 1  (COMDAT)
.debug$S hdr 0x104, chars 0x128:  40 00 30 42   -> 0x42300040  align 3  (discardable)

Compare the third and sixth lines. Both are .rdata, and they differ by exactly one bit:

plain   .rdata:  40 00 10 40     0x40100040
COMDAT  .rdata:  40 00 10 40     0x40101040
                                ^^^^^^^
                          0x1000 = IMAGE_SCN_LNK_COMDAT

Nothing about the two sections' contents differs in the header. They are both read-only initialised data with 1-byte alignment. The LNK_COMDAT bit is the linker's instruction that this section is a candidate for deduplication, and the detail of how to deduplicate it lives in the symbol table, in the aux record's Selection and CheckSum fields. That is the subject of the COMDAT concept.

One more check that the decode is right, and it is the same kind of self-consistency test used for the file header. The .text section's first instruction is a push rax (50) at file offset 0x17C, and .text is marked MEM_EXECUTE | MEM_READ with no MEM_WRITE. A writable code section would be a red flag to every platform's security model; here the flags and the bytes agree, which is what you would expect from a compiler that got it right.

Try It Yourself

$ clang --target=x86_64-pc-windows-msvc -c sample.c -o sample_msvc.obj
$ llvm-readobj --sections sample_msvc.obj
$ xxd -s 0x38 -l 4 sample_msvc.obj

Then reproduce the bug deliberately, because seeing it happen is the point. Decode 0x60500020 both ways in any language:

// WRONG: treats alignment as a set of flags
for name, mask in [("1B",0x00100000),("2B",0x00200000),("4B",0x00300000),
                   ("8B",0x00400000),("16B",0x00500000),("32B",0x00600000)]:
    if 0x60500020 & mask == mask: print("reports", name)

// RIGHT: treats it as a field
print("align =", (0x60500020 >> 20) & 0xF, "= 16 bytes")

What to look for: the wrong version prints several alignments for one section, the right version prints one. Then check llvm-readobj's output against the right version — it prints exactly one alignment per section, and it agrees.

Worth noticing: llvm-readobj prints the flags and the raw value in parentheses, e.g. Characteristics [ (0x60500020). Reading both the number and the names is the fastest way to catch a decode bug, because a value and its flag list that do not correspond is immediately visible.

Check Your Understanding

Without looking back: a section header has Characteristics = 0x40300040. Which four flags does it carry, and what is its alignment?

Apply It

You are auditing a build and want to flag any section that will end up writable and executable in the linked image, because that combination is what modern exploit mitigations look for. You have written the test as a pair of bitmask checks. What is wrong with your approach, and what does the audit actually have to do?

The general habit: know which of a field’s bits are flags and which are numbers, and use the right test for each. But also know that decoding a file correctly and interpreting it correctly are separate skills — a perfectly decoded object header still does not tell you what the final executable will look like.

Connect

These same permission bits appear in the PE section headers, where they do describe the running image — see PE common sections and PE security flags. The ELF course has the same idea under a different name, with SHF_WRITE and SHF_EXECINSTR in sh_flags. Three formats, one question: may this page be written, and may it be executed?

That closes Module 1. You have the container: a 20-byte header that says what kind of file this is, 40-byte section headers that say where the pieces are, and a characteristics field that says what the linker should do with them.

Module 2 opens the tables that make an object relocatable — starting with the simplest one, the string pool that every other name is resolved through.

Next: The String Table — eight bytes, two meanings, and one place where the format quietly runs out.