Underlayer
LearnPathReviewProgressSearch
Your space
DashboardBookmarksNotesStudy plannerAchievementsStreaksAlertsCertificates
HelpFAQSettings
Sign in to keep your progress across lessons.Sign inor create an account
Concept 10 of 10 in Static, and Build OnePrevious: What -static Actually DoesNext
Claim your certificate

Sign in to bookmark this lesson, keep notes on it, and send corrections.

    Back to course

    Writing One Yourself

    ⏱26 min▲advanced☰Module 4: Static, and Build OneContinues from link-static-real
    26 min · Module 4: Static, and Build One · Advanced

    Why This Matters

    Nine concepts of reading, and this one writes. The tool is linklab.py, in this course’s sample directory, and it is deliberately shaped around the three things the course established rather than around convenience:

    1. A LINKER SCRIPT IS A PROGRAM YOU CAN EDIT. So get the source first, with ld --verbose, and edit a copy. Never maintain a fork from memory. 2. ITS EFFECT IS CHECKABLE, NOT ASSUMED. So every subcommand VERIFIES its own result and prints PASS or FAIL. A driver that only printed the linker's output would teach nothing the linker does not already print. 3. THE LINKER WILL NOT TELL YOU YOUR EDIT DID NOTHING. So VERIFY THE EDIT, before linking.

    Point 3 is the one that came from a scar. During the writing of this course, two separate measurements produced right-sized but wrong files, and both were caught only by comparing bytes or by checking the generated script actually contained what it was supposed to. That is now a standing assertion in the build script and the crosscheck, and it is the first thing linklab.py does when you ask it to move a binary.

    A Simple Model

    The tool, and what each subcommand demonstrates.

    $ python3 linklab.py
      show      print the default linker script ld is using
      base      move the whole binary to an address,
                verify, and RUN it
      budget    a MEMORY script with a real, enforced budget
      order     the four-cell first-match-wins experiment
      gc        reachability with and without --gc-sections
      tour      all of the above, each step verified
    

    Every one of those is a concept in this course, expressed as an operation that ends in a PASS or a FAIL. The interesting part is what each one has to check, because the check is the content:

    base the edit changed 2 occurrences, and we say so -- because 0 would mean the default script changed shape and we would be silently doing nothing. then: did the link succeed? is the first PT_LOAD at the address we asked for? is the e_type EXEC? DOES IT RUN? and the last one is not decoration. a script is a claim about the machine, and the machine gets a vote. budget does LENGTH=4M link? did .text land at the rom ORIGIN? did the NON-allocated sections stay at 0? then LENGTH=64: does it REFUSE, does the error name the region, and does it say by how many bytes? order the four cells, and it prints each result before judging them, so a wrong conclusion is visible rather than summarised.

    Does It Actually Work?

    The whole tour, verbatim:

    $ python3 linklab.py tour
    
    === linklab: a linker script is a program ===
    
    The script ld is using right now is 276 lines long.
    
    Move the whole binary to 0x3000000
       replaced 2 occurrence(s)
       PASS link succeeded
       PASS first PT_LOAD is at 0x3000000
       PASS e_type is EXEC (a script implies a fixed model)
       PASS and the program RUNS at that address (exit 0)
       PASS some PT_LOAD is marked executable
       PASS and it is at or above the first load (headers come first)
    
    A MEMORY script with a 4M budget
       PASS LENGTH=4M links
       PASS   .text landed at the rom ORIGIN
       PASS   and non-allocated sections stayed at 0
       PASS LENGTH=64 refuses the link
       PASS   and the error names the region
       PASS   and says by how much
    
    Which rule wins? Four cells, and the answer is not what I expected
       keep only              .labnote kept: True
       discard only           .labnote kept: False
       keep FIRST, discard 2nd .labnote kept: True
       discard 1st, keep 2nd  .labnote kept: False
       PASS first-match-wins in all four cells
       so /DISCARD/ is NOT special: in cell 3 it is simply the last
       rule, and in cell 4 simply the first.
    
    --gc-sections is reachability, not liveness
       -O1 --gc-sections off      lab_* survivors: lab_also_dead lab_dead lab_used  .text=0x123
       -O1 --gc-sections on       lab_* survivors:                              .text=0xf3
       -O0 --gc-sections off      lab_* survivors: lab_also_dead lab_dead lab_used  .text=0x152
       -O0 --gc-sections on       lab_* survivors: lab_used                     .text=0x122
       PASS at -O1 the collector removed lab_used
       PASS at -O0 the collector KEPT lab_used
       the only difference is inlining: at -O1 main folds lab_used(41) to 42,
       so the out-of-line copy has no caller.
       PASS and .text shrank by exactly the collected functions (0x30)
    
    Every step verified.
    

    Twenty checks, all green, from a directory containing two files. And notice the two PASS lines that are really about the tool rather than the linker: “the program RUNS at that address” and “some PT_LOAD is marked executable”. The first existed because the tool initially asserted the first load was executable, which is wrong — the first load is the read-only ELF headers. The check caught its own author.

    Now the exercise, and it is a real one rather than a demonstration. Write a script from an empty file:

    $ printf '/* mine */\n' > mine.ld
    $ clang -O1 hello.o -o out -T mine.ld
    ld.bfd: out: error: PHDR segment not covered by LOAD segment
    

    Add OUTPUT_FORMAT and ENTRY, and link again — the next complaint is about your sections, not the headers. Add a SECTIONS block with .text only, and it links and runs. Then one section at a time:

      . = 0x400000 + SIZEOF_HEADERS;
      .text   : { *(.text .stub .text.*) }
      .rodata : { *(.rodata .rodata.*) }
      .data   : { *(.data .data.*) }
      .bss    : { *(.bss  .bss.* ) }
    

    Eleven lines, and it is a complete, working linker script for a hosted Linux program — smaller than a tenth of the default, and every line in it is one you chose. The exercise is to reach that file from an empty one by letting the linker’s errors tell you what is missing.

    A Real Example

    The difference between a script you wrote and a script you understand, expressed as one experiment. Add a section to your program that your script does not name:

      /* four sections named. add a fifth. */
    
      __attribute__((section(".mystuff"), used))
      static int mine[4] = {1,2,3,4};
      int get_mine(int i){ return mine[i & 3]; }
    
      $ clang -O1 -T mine.ld hello.o -o out
      $ ./out; echo "exit=$?"
      exit=0                       <-- IT WORKS. and that is the problem.
    
      $ readelf -SW out | grep mystuff
      [15] .mystuff  PROGBITS  0000000000900f0 ... 00  A  0  0  8
    
      ^ the orphan fallback placed it, at the END of
        everything your script placed. it ran because
        nothing was wrong yet.
    

    It works, which is exactly what makes it dangerous. The section is allocated, its address is valid, the program reads the right values, and there is no diagnostic. Now turn on the one mechanism that would have caught it:

      $ clang -O1 -T mine.ld hello.o -o out -Wl,--orphan-handling=warn 2>&1
      warning: orphan section `.mystuff' from `hello.o' being placed
      in section `.mystuff'
    
      $ clang -O1 -T mine.ld hello.o -o out -Wl,--gc-sections -Wl,--orphan-handling=warn
      warning: orphan section `.mystuff' from `hello.o' being placed
      in section `.mystuff'
      $ ./out; echo "exit=$?"
      exit=139                    <-- SEGFAULT
    

    Add --gc-sections and the program now crashes. Because the orphan fallback and garbage collection interact: the fallback placed the section, but the reachability walk did not mark it live, and --gc-sections removed it. Two features that each work correctly, composing into a failure — and the fix is one line:

      .mystuff : { KEEP (*(.mystuff)) }
                                   ^^^^^
      and now both mechanisms agree: your script owns the
      section, it is placed where you said, and it survives.
    

    That composition is the most useful thing in this concept, because it is the shape of real linker-script bugs. You will not be handed a script that is missing a section and a build with --gc-sections on the same day on purpose; you will add the section in March, and somebody will add the flag in September, and the program will start crashing in a build that nobody touched. Neither change was wrong. The composition was. And a one-line script that names and KEEPs the section makes the class impossible.

    Which is the argument for owning your script. The default script would have handled the section by the fallback, and the fallback and --gc-sections would have disagreed in exactly the same way — so the danger is not having a custom script, it is having an implicit one. A script you wrote is a script you can read.

    Try It Yourself

    $ cd courses/link/assets/samples
    $ python3 linklab.py tour
    $ python3 linklab.py base 0x2000000
    $ python3 crosscheck.py
      ALL 124 CHECKS PASS
    

    Then extend the tool, which is the exercise rather than the demonstration:

      1. Add a `stack` subcommand that puts .bss at a
         fixed address with a KEEP, using a MEMORY region.
         Then check that the section really is there, not
         just that the link succeeded.
    
      2. Add a `probe` subcommand that reads any
         SEGMENT_START or CONSTANT out of a script and
         prints it. It needs the -u trick from the first
         concept -- PROVIDE is conditional, so a symbol
         nothing references is never emitted. Verify the
         -u is in there before you wonder why it prints 0.
    
      3. Add a `diff` subcommand: dump two scripts and
         show only the lines that differ. This is the tool
         you actually want when a build changes size and
         you do not know why, and it is about fifteen lines.
    
      4. Make `order` print a warning if any of its four
         generated scripts does NOT contain the rule it is
         supposed to. That check is the direct descendant of
         the bug that made this course briefly claim
         /DISCARD/ overrides rule order -- a no-op string
         replace looks exactly like a linker obeying you.
    
      5. Finally, and this is the real test: delete
         build_samples.sh, keep the three scripts, and check
         that crosscheck.py still passes from an empty
         directory. It should not -- the specimens are
         generated -- which tells you exactly which files are
         sources and which are products. Knowing that about
         your own build is the same skill.
    

    Exercise 3 is the one that becomes useful immediately and forever. A diff of two linker scripts is the fastest way to answer “why did my binary get 4 KB bigger” when you cannot remember what you changed — and it works because both scripts are text, which is the property that made this course possible. Before ld --verbose existed as a workflow, that question had exactly one answer, which was “read the linker source”.

    Exercise 4 is the one that makes the tool trustworthy, and it is deliberately placed here rather than earlier. A tool that generates inputs must verify the inputs it generated, because the failure mode of a no-op edit is indistinguishable from correct behaviour. That is a lesson about verification rather than about linkers, and it is the one that has bitten this course twice.

    Connect

    This is the end of the course, and it is the concept that makes the other nine into a skill rather than knowledge. The script is source you can read; eight constructs; first-match-wins; the dot; budgets; segments, and the -T trap; reachability and KEEP; the fallback; what static costs. And now a tool that does all of it and checks the answer.

    The chain this course sits in is almost complete, and it is worth seeing where each piece ended up. The Object Files course established what a relocation record is and, at the end, wrote a 936-byte ELF object byte by byte. The Symbol Resolution course established who decides what a name means, and traced the map file. The Relocations course established the vocabulary and wrote a relocation applier that accepted a layout as input and said so in a comment. This course is that comment's subject.apply_relocs.py had a build_layout() function and a note that deciding the layout is “a different program with a much harder problem” — and a linker script is that program, written in a language with eight constructs and no control flow.

    The next link in the chain is the one this course hands on, and it is not another course. It is a thing you can run.linklab.py is 250 lines, needs nothing but a compiler and ld, and every subcommand ends in a verdict. If the mission of the collection is a learner who can build a compiler that targets any architecture and emits executables without a backend, then this is one of the small pieces of that: a placement engine you can read end to end and change, driving a real linker and verifying the result by running it.

    Three debts the earlier courses left, settled here. The -. != 0 conditional inside .bss from the location-counter concept is a real unsolved question with a two-line answer written after it — which is what ordinary source code looks like, and worth knowing that the toolchain is ordinary. The PROVIDE conditionality from the linker-defined symbols concept is the -u trick, and it is now a documented feature of the tool rather than a surprise. And the map file from the resolution algorithm concept is where the linker records every decision this course makes about placement, including the ones it guessed — which makes it the tool you reach for when any of this has gone wrong in a build you did not write.

    And the last connection, to the platform you are reading this on. The Underlayer server is built with this toolchain, in Chemical, and it serves these pages. Module 2’s finding — that -T overrides the code model — is a fact that applies to the binary running right now, and the fact that you can read the script driving it is a property of the toolchain, not of the language. Nothing prevents the same inspection here: the server is an ELF file, ld --verbose is one command, and the 276 lines that placed it are readable. That is the through-line of the whole collection, and it is not a metaphor.

    Previous: What -static Actually DoesEnd of course · back to the course page