Chapter 6: The toolchain¶
What: the set of programs that turn your C and assembly source into a binary your i.MX6ULL will execute.
Why: every later chapter ends with “now build it.” If you do not know what the build tools are doing, build failures become guesswork.
Focus: (a) that
gccis a driver over half a dozen smaller tools. (b) that ELF is the universal container, and the linker decides where every byte ends up. (c) that the ABI is the contract that every function call in your program follows.
ELF: Executable and Linkable Format, the standard Linux object and executable file format.
ABI: Application Binary Interface: the calling convention, register use, binary format, and library contract that let separately built code run together.
6.1 gcc is not one program¶
When you type:
$ arm-none-linux-gnueabihf-gcc -O2 -o hello hello.c
The gcc command coordinates several build stages. GCC is a driver: it parses the command line, selects the required tools, and passes each stage’s output to the next stage.
The main build stages are:
cpp, preprocessor. Resolves#include, expands macros, strips comments. Output: pure C, no directives. Try-Eto stop here.cc1, the C compiler proper. Parses C, builds an internal IR (RTL/GIMPLE), optimizes, lowers to target assembly. Output: a.sfile. Try-Sto stop here.as(from binutils), assembler. Turns.sinto a relocatable.o(ELF object file). Try-cto stop here.collect2, when used by this GCC build. It wraps the linker and helps arrange constructor initialization. You normally do not invoke it directly.ld(from binutils), linker. Combines.ofiles and libraries, resolves symbol references, applies the linker script’s address layout, and writes the final ELF executable.
For a dynamically linked program, the final ELF also names a dynamic loader. That loader runs later on the target when the program starts. It is not a stage of the host compile command.
You can see the chain by adding -v to any compile:
$ arm-none-linux-gnueabihf-gcc -v -o hello hello.c 2>&1 | head -20
Using built-in specs.
COLLECT_GCC=arm-none-linux-gnueabihf-gcc
COLLECT_LTO_WRAPPER=/home/<you>/imx6ull/toolchains/arm-gnu-toolchain-<version>-x86_64-arm-none-linux-gnueabihf/libexec/gcc/arm-none-linux-gnueabihf/<version>/lto-wrapper
Target: arm-none-linux-gnueabihf
...
Common failures include incorrect include paths during preprocessing, incompatible object files during linking, and missing libraries or startup files in the sysroot.
6.2 The binutils inventory¶
binutils is a collection of tools that operate on object files and binaries. Cross-prefixed versions exist for every target. The ones you will use:
Tool |
What it does |
When you reach for it |
|---|---|---|
|
Assemble |
Usually invoked through |
|
Link object files and libraries into an ELF |
Bare-metal builds using |
|
Convert formats and extract sections |
Convert an ELF into a raw binary |
|
Disassemble code and display headers or sections |
Inspect generated instructions |
|
List symbols in an object |
Find where a symbol is defined or exported |
|
Display ELF metadata |
Inspect sections, segments, and dynamic information |
|
Remove symbols/debug info |
Producing the shipped binary. |
|
Build/dissect |
When making your own libs. |
|
Map address → file:line |
Decoding crash addresses, oopses. |
|
Print section sizes |
Quick sanity check on memory budget. |
Two examples to internalize.
objdump -d on a bare-metal LED¶
$ arm-none-eabi-objdump -d hello.elf | head -25
hello.elf: file format elf32-littlearm
Disassembly of section .text:
00900000 <_start>:
900000: ea000001 b 90000c <main>
00900004 <_irq_handler>:
900004: eafffffe b 900004 <_irq_handler>
0090000c <main>:
90000c: e3a00d22 mov r0, #0x2200
900010: e3400000 movt r0, #0
900014: e5803000 str r3, [r0]
900018: eafffffe b 900018 <main+0xc>
This output lets you verify the instructions and addresses produced by the toolchain.
readelf -l to see segments¶
$ arm-none-linux-gnueabihf-readelf -l hello
Elf file type is DYN (Shared object file)
Entry point 0x4c0
There are 9 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00120 0x00120 R 0x4
INTERP 0x000154 0x00000154 0x00000154 0x00019 0x00019 R 0x1
[Requesting program interpreter: /lib/ld-linux-armhf.so.3]
LOAD 0x000000 0x00000000 0x00000000 0x004f4 0x004f4 R 0x10000
LOAD 0x000ed4 0x00010ed4 0x00010ed4 0x0011c 0x0011c RW 0x10000
...
Notice: the segment marked INTERP says the dynamic linker for this binary is /lib/ld-linux-armhf.so.3. That is what runs first when the kernel execs this file. Only after the dynamic linker finishes loading shared libraries does control reach main.
For bare-metal output we will not have an INTERP segment. The ELF will be statically resolved and the entry point we set is what runs.
6.3 Section, segment, VMA, LMA¶
These terms describe three different viewpoints: the compiler, the linker, and the loader.
Word |
Who mainly cares? |
Meaning |
|---|---|---|
Section |
Compiler and linker |
A named bucket of related bytes: |
Segment |
Loader |
A loadable memory range described by ELF program headers. One segment can contain several sections. |
VMA |
CPU at runtime |
The address the code/data expects to have when it is being used. |
LMA |
Loader/startup code |
The address where the initial bytes are stored before they are moved to their runtime address. |
If you come from MCU work, start with sections. You already know these:
.text: executable instructions..rodata: constants and string literals..data: globals/statics with non-zero initial values, such asint led = 1;..bss: globals/statics that start as zero, such asint counter;.
The linker script arranges sections. The loader does not want to reason about every tiny section. It wants bigger chunks it can load or map with permissions:
sections: .text .rodata .data .bss .debug_*
| | | |
v v v v
segments: LOAD R-X LOAD RW debug is not loaded
So a segment is the loader-facing package. For a Linux process, the kernel and dynamic linker read the ELF program headers, create mappings for the LOAD segments, and eventually call into the program. For our bare-metal image, the “loader” is usually the Boot ROM, U-Boot, uuu, or our own startup code.
Now the address pair:
VMA answers: “Where will this section live when the CPU uses it?”
LMA answers: “Where are the bytes stored in the image before runtime setup?”
In a Linux process, a VMA is normally a virtual address. In early bare-metal code, before the MMU is enabled, it is usually the physical address from which the CPU executes or accesses data. For this chapter, think of VMA as the runtime address.
Most simple programs have VMA = LMA. If the Boot ROM loads your whole image into OCRAM at 0x00907400, and your code also runs from 0x00907400, the load address and runtime address match:
image in OCRAM:
.text VMA 0x00907400, LMA 0x00907400
.data VMA 0x00908000, LMA 0x00908000
The useful case is when where bytes are stored differs from where bytes must run. Classic Cortex-M Flash + RAM does this every day:
Flash image RAM after startup
----------- -----------------
.text runs from Flash .data variables live here
.rodata stays in Flash .bss zeroed here
.data initial values ----copy---> .data runtime values
For .data in that system:
Address kind |
Example meaning |
|---|---|
VMA |
RAM address where |
LMA |
Flash address where the initial value of |
That is why startup code copies .data from LMA to VMA before main(), then zeros .bss. In linker-script language, AT(addr) is how you say “this section runs over here, but its initial bytes are loaded over there.” Chapter 10 uses this pattern with AT(_etext).
Linux user-space hides most of this because the kernel and dynamic linker perform the load/mapping work. Bare-metal code cannot hide it. On the i.MX6ULL, the distinction returns whenever a small image starts in OCRAM, initializes DDR, then moves code or data into DRAM.
6.4 Linker scripts¶
A linker script (.ld file) is a small text file that tells ld:
What memory regions exist and their attributes.
Which sections go into which regions.
Where the entry point is.
What symbols to export (
_etext,_sdata,_edata,_sbss,_ebss).
The simplest useful linker script for our Chapter 9 bare-metal LED:
ENTRY(_start)
MEMORY
{
OCRAM (rwx) : ORIGIN = 0x00907000, LENGTH = 0x00019000 /* ~100 KB */
}
SECTIONS
{
.text : { *(.vectors) *(.text*) *(.rodata*) } > OCRAM
.data : { *(.data*) } > OCRAM
.bss : { _sbss = .; *(.bss*) *(COMMON) _ebss = .; } > OCRAM
_stack_top = ORIGIN(OCRAM) + LENGTH(OCRAM);
}
Five things to notice:
ENTRY(_start)records the ELF entry symbol. The Boot ROM uses the IVT entry field instead, but debuggers and ELF tools use this value.MEMORYdeclares one region calledOCRAMof ~100 KB. Permissions (rwx) are advisory for now.SECTIONScombines input sections into output sections. The> OCRAMplacement assigns each output section to the next available address in OCRAM._sbss = .; ... _ebss = .;exports the bounds of.bssso our startup code can clear it._stack_topis computed at link time as one address past the end of the declared OCRAM region. Startup code loadsspfrom this symbol.
We will revise this script over the next chapters as we move to DDR. The format does not change. Only the regions do.
6.5 The ABI: what makes function calls work¶
When foo() calls bar(), both sides must agree on:
Which register contains the first argument? The second? Where do return values live?
Which registers must
bar()preserve, and which can it freely clobber?How is the stack aligned?
Where do floating-point arguments go, in integer registers, or in FPU registers?
How are structs > 8 bytes returned?
The EABI (Embedded ABI) for ARM specifies all of this. ARMv7-A Linux uses the AAPCS (ARM Architecture Procedure Call Standard) plus the EABI’s runtime conventions. The relevant variant for us is AAPCS-VFP, also called “hard-float,” in which floating-point parameters use s0-s15 / d0-d7 rather than integer registers.
The core rules (simplified):
Register |
AAPCS role |
|---|---|
r0-r3 |
First four integer arguments / return value ( |
r4-r11 |
Callee-saved (must be preserved). R9 is “platform register”, see §6.6. |
r12 (ip) |
Intra-procedure-call scratch. Caller-saved. |
r13 (sp) |
Stack pointer. 8-byte aligned at function boundary. |
r14 (lr) |
Link register (return address). |
r15 (pc) |
Program counter. |
s0-s15 / d0-d7 |
First eight FP arguments / FP return. Caller-saved. |
s16-s31 / d8-d15 |
Callee-saved FP. |
Why this matters: when you write a function in assembly and call it from C (or vice versa), you must obey AAPCS or memory corruption follows. The toolchain assumes it. You must too.
Hard-float vs soft-float¶
Three flavors of FP ABI exist:
soft-float (
-mfloat-abi=soft), FP ops are emulated in libgcc. FP arguments go in integer registers. Slow but universally compatible.softfp (
-mfloat-abi=softfp), FP ops use the FPU, but FP arguments still go in integer registers. Compromise, used when linking soft-float libraries with code that has an FPU.hard-float (
-mfloat-abi=hard), FP ops use the FPU. FP arguments use FP registers. Fastest.
The triplet suffix tells you which: arm-none-linux-gnueabi (soft), arm-none-linux-gnueabihf (hard). You cannot link a soft-float .o with a hard-float .o. The linker refuses.
The Linux toolchain selected for this book uses the hard-float ABI. All Linux user-space objects and libraries that we combine must use the same ABI.
6.6 The C library, or its absence¶
For bare-metal code in Part II, we want no libc at all. We will write our own memcpy and our own printf. This keeps the early examples explicit: every dependency and hardware assumption is visible.
The C library, or libc, is separate from GCC. GCC is the compiler driver and compiler. Libc is a runtime library plus headers that the compiler links against when you build Linux user-space programs. A complete cross-toolchain usually ships all of these together:
Piece |
Example |
Job |
|---|---|---|
Compiler |
|
Turns C into object files. |
Binutils |
|
Assembles, links, converts, and inspects binaries. |
libc headers |
|
Tell the compiler what user-space APIs look like. |
libc binaries |
|
Provide the code that implements the C/POSIX runtime. |
libgcc |
|
Small helper routines that GCC itself may need, such as integer division helpers. |
Those headers and libraries live in the toolchain’s sysroot: a directory that looks like a tiny target root filesystem, containing target headers and target libraries. So people say “the Ubuntu ARM GCC toolchain ships glibc,” but glibc is not inside the compiler executable. It is packaged alongside the compiler and selected by the compiler when linking.
For Linux user-space code, we use a libc. Three options:
libc |
Size of typical static |
Notes |
|---|---|---|
glibc |
~700 KB |
Common on general-purpose Linux distributions. Broad compatibility. |
musl |
~30 KB |
Small, MIT-licensed implementation often used in compact systems. |
uClibc-ng |
~50 KB |
Maintained fork of uClibc, available in Buildroot and used by some embedded distributions. |
We will mostly use glibc because the Ubuntu toolchain ships it. In Chapter 34 we switch to musl once for comparison.
What libc actually provides¶
A libc bundles:
Wrappers around syscalls (
open,read,write,mmap, …) so you can call them as C functions.Memory allocator (
malloc,free, internally callingbrk/mmap).Standard I/O (
fopen,printf), buffered layers atop the syscall wrappers.Math (
sin,sqrt, …), inlibm.so.POSIX threads (
pthread_*), sometimes a separatelibpthread.so, sometimes folded in.Locale, time, network, etc.
Bare-metal code does not receive these services automatically. There is no malloc, printf, or errno unless our program or another linked library implements it.
6.7 Make, in working depth¶
make runs the builds for the bare-metal projects in Part II and for U-Boot, Linux, and Buildroot later in the book. This section explains the parts used by those builds.
Before syntax, understand the job.
make decides which build commands need to run. It does not know C, assembly, ELF, or ARM by itself. You teach it:
Which files you want to create.
Which input files each output depends on.
Which shell command creates the output from the inputs.
Then make answers one question: what commands need to run right now?
It answers by looking at files and timestamps:
If the output file does not exist, build it.
If an input file is newer than the output file, rebuild it.
If the output exists and all inputs are older, skip it.
For our bare-metal LED program, the dependency chain looks like this:
startup.S ──► startup.o ┐
├──► led.elf ──► led.bin
main.c ──► main.o ┘
link.ld ──────────────┘
If you edit main.c, only main.o, led.elf, and led.bin need rebuilding. startup.o can be reused. If you edit link.ld, the object files can be reused, but led.elf and led.bin must be rebuilt. This is why make exists: it avoids rebuilding everything when only part of the input changed.
A Makefile records those dependency relationships and the shell commands that produce each output. make decides whether to run the commands and in what order.
When you run plain make, it reads a file named Makefile in the current directory and builds the first target in that file. In our examples, the first target is usually all, and all depends on the final file we want, such as led.bin.
6.7.1 Rule shape¶
target ...: prerequisite ...
<TAB>command
<TAB>command
In a rule, the target is the file you want to create. The prerequisites are the files it depends on. The indented commands are the recipe that creates or updates the target.
make builds the target when the target does not exist or when a prerequisite is newer than the target. Recipe commands must be indented with a literal TAB. Spaces do not work.
6.7.2 Variables: four flavors of assignment¶
CC = arm-none-linux-gnueabihf-gcc # 1) recursive ("deferred")
CC := arm-none-linux-gnueabihf-gcc # 2) simple ("immediate")
CFLAGS ?= -O2 # 3) only if not already set
OBJS += extra.o # 4) append
The pair people misunderstand most is = vs :=:
name = world
greet = hello $(name)
name = there
$(info $(greet)) # prints "hello there" ← deferred expansion
name := world
greet := hello $(name)
name := there
$(info $(greet)) # prints "hello world" ← immediate expansion
Use := everywhere by default. The = form is occasionally necessary (recursive expansion of generated variables), but it is easier to misuse.
6.7.3 Pattern rules and automatic variables¶
A pattern rule with % matches every file fitting the pattern:
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
Inside the recipe, automatic variables carry the per-instance pieces:
Var |
Meaning |
|---|---|
|
The target being built |
|
The first prerequisite |
|
All prerequisites (de-duplicated, space-separated) |
|
All prerequisites (with duplicates) |
|
Prerequisites newer than the target |
|
The stem matched by |
A pair of pattern rules and one main rule, plus a clean phony, is 90% of every Makefile you will write in this book.
6.7.4 Phony targets¶
.PHONY: all clean install
Tells make that all / clean / install are not filenames. Without .PHONY, a file named clean would make make clean consider the target up to date and skip the recipe. With .PHONY, the recipe always runs.
6.7.5 Useful functions¶
make has a small set of built-in functions, called as $(name args,...):
Function |
What it does |
Example |
|---|---|---|
|
List files matching a glob (no quoting) |
|
|
Pattern substitution |
|
|
Plain string substitution |
|
|
Directory part |
|
|
Filename part |
|
|
Drop the extension |
|
|
Append / prepend |
|
|
Keep / remove matching |
|
|
Sort + de-duplicate |
|
|
Run a shell command, capture stdout |
|
A common idiom, collect every .c in the tree:
SRCS := $(wildcard bsp/*/*.c) $(wildcard *.c)
OBJS := $(patsubst %.c,%.o,$(SRCS))
6.7.6 Conditionals¶
ifeq ($(ARCH),arm)
CFLAGS += -mcpu=cortex-a7
else ifeq ($(ARCH),aarch64)
CFLAGS += -mcpu=cortex-a53
else
$(error Unsupported ARCH=$(ARCH))
endif
ifdef DEBUG
CFLAGS += -O0 -g3
else
CFLAGS += -O2
endif
Four conditional forms: ifeq, ifneq, ifdef, ifndef. They work both at the top level (selecting variable values) and inside recipes, though for recipe-level branching, shell if is usually cleaner.
6.7.7 Parallelism¶
$ make -j$(nproc) # use all available cores
$ make -j8 # 8 jobs in parallel
For our bare-metal builds (~10 files), -j makes no measurable difference. For the kernel (~30 000 files) it cuts build time by ~7× on an 8-core host. Always use it for kernel work. Harmless for everything else.
6.7.8 A complete Makefile for the Chapter 9 LED¶
CROSS := arm-none-eabi-
CC := $(CROSS)gcc
LD := $(CROSS)ld
OC := $(CROSS)objcopy
CFLAGS := -mcpu=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard \
-ffreestanding -fno-builtin -nostdlib -O2 -g -Wall
SRCS := startup.S main.c
OBJS := startup.o main.o
all: led.bin
%.o: %.S
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
led.elf: $(OBJS) link.ld
$(LD) -T link.ld -o $@ $(OBJS)
led.bin: led.elf
$(OC) -O binary $< $@
clean:
rm -f *.o *.elf *.bin
.PHONY: all clean
Every flag in CFLAGS matters:
-mcpu=cortex-a7: generate code that uses Cortex-A7 features.-mfpu=neon-vfpv4 -mfloat-abi=hard: match what the silicon supports and the ABI we picked.-ffreestanding: “I do not have a hosted C environment.” Disables the assumption thatmainis the standard entry, etc.-fno-builtin: disables compiler’s optimization of calls likeprintfinto special builtins.-nostdlib: do not implicitly linkcrt0, libc, libgcc. (We will manually add libgcc later if we need its compiler-rt routines.)-O2 -g: optimize but keep debug info.-Wall: turn on the warnings everyone should be using.
6.8 Static vs dynamic linking (for Linux user-space)¶
Two ways to combine your code with libraries:
Static. Library code is copied into your binary at link time. The binary is self-contained. No
libfoo.sois needed at runtime. Bigger file. Faster startup.Dynamic. Library code lives in
.sofiles on disk. Your binary references them by name. The dynamic loader (/lib/ld-linux-armhf.so.3) resolves them at process start.
Linux distributions normally use dynamic linking. Small embedded systems may use either model. Static linking simplifies deployment for a few standalone programs, while dynamic linking saves storage when many programs share the same libraries.
To force static:
$ arm-none-linux-gnueabihf-gcc -static -o hello hello.c
$ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
statically linked, BuildID[sha1]=..., with debug_info, not stripped
Compare sizes:
$ arm-none-linux-gnueabihf-gcc -o hello-dyn hello.c
$ ls -l hello-dyn
$ arm-none-linux-gnueabihf-gcc -static -o hello-stc hello.c
$ ls -l hello-stc
Expect roughly 8 KB dynamic vs 600 KB static with glibc, or ~30 KB static with musl.
6.9 ELF structure needed for this book¶
An ELF file has:
┌──────────────────────────────┐
│ ELF Header │ ← architecture, type (REL/EXEC/DYN), entry point
├──────────────────────────────┤
│ Program Header Table │ ← segments (loader's view)
├──────────────────────────────┤
│ .text │
│ .rodata │
│ .data │
│ .bss (no actual bytes) │
│ .symtab, .strtab │
│ .debug_* (DWARF) │
│ ... │
├──────────────────────────────┤
│ Section Header Table │ ← sections (linker/debugger's view)
└──────────────────────────────┘
A few key facts:
Type
REL(relocatable,.o), produced by the assembler, fed to the linker.Type
EXEC(executable), produced by linking statically.Type
DYN(shared object / PIE executable), relocatable at load time, used for both.solibraries and modern position-independent executables..bssoccupies no file bytes. It only declares “give me N bytes of zero at runtime.” The startup code (or the kernel) zeroes it.DWARF is the debug-info format used in
.debug_*sections.gdb,objdump -S, andaddr2lineread it.
When gdb says “no debug info, no symbols”, it means the binary was stripped (strip removed the symbol and DWARF sections).
6.10 Lab¶
Two builds. Both reproducible from a clean checkout.
Lab A, Host hello world, inspected¶
$ cat > hello.c <<'EOF'
#include <stdio.h>
int main(void) { puts("hello"); return 0; }
EOF
$ gcc -g -O2 -o hello-host hello.c
$ arm-none-linux-gnueabihf-gcc -g -O2 -o hello-arm hello.c
$ file hello-host hello-arm
$ readelf -a hello-arm | head -40
$ arm-none-linux-gnueabihf-objdump -d hello-arm | grep -A 5 '<main>:'
Read the disassembly. Find the bl puts instruction (or its inline equivalent). Find where r0 is loaded with the address of the string "hello".
Lab B, Bare-metal LED skeleton (build only. We’ll add the LED code in Ch 9)¶
Create ~/imx6ull/src/ch06-skeleton/ with:
startup.S: a minimal startup that sets SP and branches tomain.main.c:int main(void){ while(1); return 0; }.link.ld: the minimal script from §6.4.Makefile: from §6.7.
Run make. You should get led.elf and led.bin.
Then:
$ arm-none-eabi-size led.elf
$ arm-none-eabi-readelf -S led.elf
$ arm-none-eabi-objdump -d led.elf
$ arm-none-eabi-nm led.elf | sort
In your journal, answer:
How big is
.textin bytes?How big is
.bss? Why does it consume no space inled.bin?What is the address of
_start?What is the address
nmreports for_stack_top? Does it matchORIGIN(OCRAM) + LENGTH(OCRAM)?
Compare the reported sections, symbols, and addresses with the linker script. Use readelf -a when you need additional ELF details.
6.11 Pitfalls¶
Mixing incompatible toolchain outputs. Do not link bare-metal objects from
arm-none-eabi-into Linux user-space programs built witharm-none-linux-gnueabihf-. They have different runtime assumptions. The failure may appear only at link time and may mention an ABI or relocation mismatch.-nostdlibalso removes the automatic libgcc link. If code uses an operation such as 64-bit integer division, GCC may emit a call to__aeabi_uldivmodfromlibgcc. Add-lgccexplicitly after your object files when required.Linker-script order matters. Place a specific startup section before a broader wildcard such as
*(.text*)when startup must appear first. Chapter 9 shows the required ordering..bssmust be zeroed. If startup does not clear.bss, uninitialized globals contain old memory values and program behavior can change between boots.Wrong
-march/-mcpu. Toolchain defaults vary. Always specify-mcpu=cortex-a7explicitly for Cortex-A7 code. The compiler then schedules instructions for that pipeline.stripon the binary you wanted to debug. Keep an unstripped copy. A useful convention in your Makefile:$(NAME).elfis unstripped (forgdb/objdump).$(NAME).stripped.elfis the smaller deliverable.
6.12 Going deeper¶
Linkers and Loaders by John Levine. A detailed explanation of linkers and loaders.
The ELF Specification (latest is the System V ABI ed. 4.1. The AAPCS additions are in ARM IHI 0042).
The GCC manual, at least the section on language-independent options.
Procedure Call Standard for the Arm Architecture (AAPCS32), ARM IHI 0042.
man elf,man 5 elf,man 1 ld,man 1 ld.so.LWN: “How programs get run” (the kernel
execpath. Relevant when you write abinfmt).
Next chapter: Chapter 7: The Boot ROM, IVT, DCD, and BootData. With the toolchain understood, we can build images in the format expected by the Boot ROM. DCD: Device Configuration Data: ROM-executed register writes that prepare clocks and DDR before your code runs.