The data section is the element section's twin: it fills memories instead of tables. It has three forms rather than eight, it is the last section in the file, and it brings with it a section that exists for no other reason — datacount, which appears before the code section purely so the code section can be checked against it.
That section is the most interesting thing here, because it inverts the format's normal shape. Every other section is self-contained: the type section is valid on its own, the code section is valid on its own, the data section is valid on its own. The code section, though, contains memory.init and data.drop instructions that name data segment indices — and those instructions live in a section that appears before the data section that defines them. Without help a validator would have to either accept the indices unchecked or read the whole file twice.
So the format inserts a four-byte section between the element section and the code section containing nothing but a count. It is a forward declaration, and it is the only section in the format whose entire purpose is to make a later section checkable. It is a compromise that trades bytes for single-pass validation, and the reason the compromise was necessary rather than merely convenient is worth understanding.
Three forms, and the split is the same one the element section made — active or passive, implicit or explicit memory — minus the expression variant, because a data element is always a raw byte string:
There is no expression form for a data segment, and there never will be one — the element section has four extra forms because a table element can be a computed reference, and a data element is bytes. That is not an oversight in the element section's design; it is the reason the two sections have different shapes, and it is the clearest evidence that the eight element forms were added for a capability the data section never needed.
And notice what the byte count is doing: it is a size in bytes, not a count of elements. A data segment is an opaque string, so the only way to know where it ends is to be told its length. This is the first place in the format where a payload is a counted blob rather than a sequence of self-describing records, and it is why a data section cannot be walked by reading structure — you read the count and then skip that many bytes.
All three forms, each in its own minimal module, hand-assembled and each accepted by wasm-validate and read back by wabt. The segment bytes first, then the readers' verdicts:
form 0 00 41 00 0b 04 61 62 63 64
| | | | +------- "abcd", four raw bytes
| | | +---------- size = 4
| | +---------------- offset: i32.const 0 ; end
| +------------------- flags = 0, active into memory 0
+-----------------------
form 1 01 04 70 61 73 73
| | +----------- "pass"
| +-------------- size = 4
+------------------ flags = 1, PASSIVE: no memory, no offset
form 2 02 00 41 00 0b 04 61 62 63 64
| | | | |
| | | | +------- "abcd"
| | | +------------ size = 4
| | +--------------- offset: i32.const 0 ; end
| +------------------ memory index = 0
+---------------------- flags = 2, active into an EXPLICIT memory
And the two readers, independently:
$ wasm-validate data0.wasm && echo VALID
VALID
$ wasm-objdump -x data0.wasm | sed -n '/^Data/,$p'
Data[1]:
- segment[0] memory=0 size=4 - init i32=0
- 0000000: 6162 6364 abcd
...and for the passive one:
- segment[0] passive size=4
- 0000000: 7061 7373 pass
Three observations, each of which is a place a reader can go wrong. Form 0 and form 2 differ only by one byte — the explicit memory index — and for a module with one memory they are semantically identical, which is exactly the pattern the element section followed and exactly the reason a reader that treats 0x00 and 0x02 as unrelated shapes will miss the difference. The passive form has no offset and no memory index at all, so its size byte sits immediately after the flags byte, and a reader that expects an offset expression will read the size as an opcode. And the data is raw: 61 62 63 64 is "abcd" and there is no encoding step, no escaping, no NUL terminator, no length prefix beyond the section's own framing.
That last point is worth contrasting with the string encoding in the PE import directory, where names are NUL-terminated, or a COFF symbol name, which is 8 bytes with a length in a spare field and where longer names go in the string table. WebAssembly's data segments are the only place in the format with a genuinely arbitrary byte string, and they are length-prefixed rather than terminated — which is the right choice, because a length prefix permits embedded NULs and a terminator does not.
Here is the whole of it, in a module with one data segment:
0c 01 00 00 00
| | |
| | +-- the count: ONE data segment follows, later
| +----- payload is one byte
+-------- id 0x0c = datacount
and the section it is describing, which comes LAST:
0b 01 00 41 00 0b 04 61 62 63 64
Four bytes carrying one number, and that number is the segment count of a section that has not been read yet. The ordering constraint is the whole design: datacount is section 12, and it must appear between the element section (9) and the code section (10), so it is the one place where a section's position is not simply ascending by id. The section ordering concept covers why the ids ascend; this is the documented exception, and it exists precisely because the code section has to come before the data section while also being checkable against it.
And a real object confirms it. The simple.o sample from this course has a data section and wabt reports it:
$ wasm-objdump -x simple.o | grep -A1 DataCount DataCount: - data count: 1
One number, and the validator uses it to check every memory.init and data.drop in the code section. Without it, memory.init 3 in a module with one data segment would have to be either trusted or deferred, and both are worse: trusting it lets a validated module trap at run time on an index the validator was supposed to have caught, and deferring means reading the data section before the code section, which the format's ordering forbids.
Validating a memory.init, which is the operation datacount exists to make possible, and it is a nice illustration of a general principle about validation order:
Two things there are worth pulling apart. The first is that datacount gives the validator the number without giving it the content — it can bound-check an index against a count it already holds, and only read the data section later when it actually needs the bytes. That is what makes single-pass validation possible, and it is why the section is a count and not a copy of the data section's header.
The second is more interesting and slightly awkward: the element section needs no equivalent, and the reason is a subtlety in the ordering rules.table.init names an element segment, and the element section (9) comes before the code section (10), so a validator reading in order has already read the element section and knows the count. The data section (11) comes after the code section, so the same validator has not. Datacount is a workaround for an asymmetry in the section ordering, and it exists only on the side that needed it.
That is worth sitting with, because it shows a format being patched in the least invasive way rather than restructured. The alternative would have been to move the data section before the code section, which would have broken every existing module, or to have the validator make two passes, which costs more than four bytes for everyone. Datacount is four bytes paid by the producer to avoid two passes by the consumer — and it is a good trade precisely because almost every module either needs it or is not much hurt by the four bytes.
The last piece is what a passive data segment is for, and it is the same pattern as a passive element segment with a different payoff. A passive segment's bytes live in the module, not in a memory, until the module runs memory.init to copy them somewhere it chose. That is how a program gets read-only constant data into writable memory, which is a real problem: WebAssembly memories are writable, and a program that wants a large table of constants has to copy them in at start-up, because the data section can only write into a memory at instantiation. A native ELF or PE image maps a read-only section straight into memory with page protections set by the loader, and needs no copy at all. WebAssembly has no page protection, so it has to copy — and the passive form is how that copy is expressed without paying for it in every module that does not need it.
$ wasm-objdump -x data0.wasm | sed -n '/^Data/,$p'
$ wasm-objdump -x simple.o | sed -n '/DataCount/,+1p'data0.wasm, data1.wasm and data2.wasm samples are in the course assets. Dump all three, confirm the flags byte differs in every one, and note that forms 0 and 2 are one byte apart. Three small files and one observation about how little separates an encoding from its sibling.04 to 05 and see what wasm-validate says. Then to 03. One byte, and the error should name the segment — read the message and note that it is a validation error rather than a truncation, which is what a counted blob buys you over a terminated one.00 in the middle of a data segment's bytes and confirm it round-trips. Then compare with how a COFF symbol name or a PE import name would have handled the same byte. Length-prefixed versus NUL-terminated is one of the most consequential small choices in any format, and this is where you can see the difference cost something.simple.o to drop section 12, then re-validate. The failure message should tell you the code section needs a data count. Then add a datacount that disagrees with the data section and confirm which one the validator believes — the answer is the datacount, which is a genuinely surprising thing to observe.memory.init should not. Those two cases are the entire rule, and writing both tests is how you find out whether your reader's condition is the right one or an approximation of it.-O2 and compare. The gap between the two is what a compiler's constant folding removes from a section that has no compression of its own, and it is the clearest picture of why the format chose raw bytes over a compressed representation.Without looking back: a module has two data segments, both passive, and its code section contains no bulk-memory instructions. It has no datacount section. Is the module valid? And separately: what does a reader that requires datacount unconditionally do to a module like this, and why is that the wrong answer?
You are writing a validator. It accepts every module with a data section, including ones whose code section contains memory.init 5 when only one data segment exists. wasm-validate rejects those. Your validator's data section reader is correct — it finds all the segments and their bytes. What is missing, what is the smallest correct change, and why does your validator currently accept a module that will trap at run time?
The general habit: a validation pass exists to make a promise about run-time behaviour. When you find a gap, ask which promise it breaks — and note that "validated code can still trap" breaks a much stronger promise than "validated code can still misbehave".
The datacount section is the clearest example in this format of a piece of metadata whose only consumer is a validator, and this collection has a few of those. A COFF section's NumberOfRelocations is read by a linker rather than by the code, and an ELF section header's sh_info is meaningful only to whichever tool interprets that section. In all three the value describes something the reader cannot see yet, and the tool that will see it has to be trusted to check it. Datacount is the strictest of the three, because the specification requires it exactly when a later section makes a claim the earlier one has to bound.
The counted-blob encoding of a data segment connects to the way every other format in this collection stores a string table or a resource blob. A COFF string table is a length-prefixed pool shared by every name in the file. A PE resource is a tree of offsets, and its data entries are a base-relative offset plus a length — the same two fields as a data segment, in a different order. A DWARF .debug_str entry is a NUL-terminated string at an offset, so it is the one case here that cannot hold an embedded NUL — which is a real limitation of that format and the reason the others chose differently.
And the passive segment's purpose — getting read-only constants into a writable memory because the format has no page protection — is the direct consequence of a design decision this course has met in several places. WebAssembly memories have no execute, read or write permissions, unlike an ELF PT_GNU_RELRO segment or a PE section's VirtualProtect flags. That simplicity is what makes a WebAssembly sandbox small enough to implement correctly in a browser tab, and it is also why the data section has to copy instead of map, and why a memory's optional maximum has to be enforced by the host rather than by the hardware.
That completes the core format. Every section has now been covered: the framing, the eight declaration and data sections, the code section, the instruction encoding, and the one section that exists only for validation. What is left is what happens before a module is a module — the object files a compiler emits, the linking and reloc sections that live in custom sections the core specification does not define, and the 5-byte LEB128 that a producer pads its section sizes to. That is the final concept.