Splitting a program across files
prerequisite
Every program so far has lived in one file. Larger programs are split up, one file per job: each file stays short enough to read, a helper written once can serve the next program too, and two people can edit different files at the same time.
You have used a second file all along without seeing it. printf is not in your program. It lives in the C library, and bl printf works because a tool called the linker finds it there and connects your call to it. This lesson does the same with a file you write yourself.
Source files, object files, and the linker
On the server a program is built in stages, and each file goes through the first two on its own:
m4expands thedefinenames in one.asmfile and writes a.sfile.- The assembler turns one
.sfile into an object file (a.ofile): the machine code for that one file. Any label the file uses but does not contain is left as a gap, with the label's name written next to it. - The linker joins every object file, plus the C library, into one program. It gives each label its final address and fills each gap by finding the name in another file.
gcc runs the assembler and the linker for you. Given several files, it makes an object file from each and links them together.
Sharing a name with .global
A label is private to its file unless the file exports it. .global name exports a label: the assembler lists the name in the object file, where the linker can match it against the gaps in other files. That is why every program has said .global main. The start-up code in the C library calls main, and it can only reach a label that is exported.
The file that uses the name declares nothing. In a file with no clamp: label, bl clamp is not an error to the assembler; it is a gap for the linker to fill.
Here is a helper file, clamp.asm, that exports three names: a function, clamp, and two pieces of data, a table of sensor readings and the number of entries in it.
// clamp.asm: one function and one table, both exported with .global.data .balign 4 .global readingsreadings: .word -12, 37, 105, 88, 250 .global nreadingsnreadings: .word 5.text// clamp(w0 = v, w1 = lo, w2 = hi) -> w0 = v held inside lo..hi// Calls nothing and uses only w0 to w2, so it needs no frame .balign 4 .global clampclamp: cmp w0, w1 b.ge cl_not_low mov w0, w1 // below the range: answer locl_not_low: cmp w0, w2 b.le cl_done mov w0, w2 // above the range: answer hicl_done: retclamp takes a value and a range in w0, w1, and w2, and returns in w0 the value moved inside the range. It calls nothing and changes none of x19 to x28, so it needs no frame.
The file has no define lines because it gives no register a name. On the server m4 reads each file by itself, so a helper that writes fp and lr needs its own define(fp, x29) and define(lr, x30) at the top: the ones in main.asm mean nothing to it.
Running the pair
main.asm walks the table, calls clamp on each reading with the range 0 to 100, and prints the reading before and after. The editor holds both files one after the other, joined the way the playground joins files: everything above the line // ---- clamp.asm ---- is main.asm, and everything below it is clamp.asm.
It prints five lines. The readings already inside the range keep their value, -12 becomes 0, and 105 and 250 both become 100:
reading 0: -12 becomes 0reading 1: 37 becomes 37reading 2: 105 becomes 100reading 3: 88 becomes 88reading 4: 250 becomes 100note
In the full playground you can keep the two files apart. Type clamp.asm into the small file-name box above the editor, press +, and move everything below the marker line into the new tab. Before it assembles, the playground joins its tabs in order, with the same marker line between them.
Building it on the server
On the server the two files stay apart. Run m4 on each, then name both on the gcc line. The first group below builds and links in one command. The second makes each object file with gcc -c (make the object file, do not link) and then links them, which is how a large program avoids rebuilding the files that did not change.
$ m4 main.asm > main.s$ m4 clamp.asm > clamp.s$ gcc main.s clamp.s -o sensors # assemble both files and link them$ ./sensors$ gcc -c main.s # main.o: clamp and readings are still gaps$ gcc -c clamp.s # clamp.o: exports clamp, readings, nreadings$ gcc main.o clamp.o -o sensors # the linker fills the gapsNow delete the line .global clamp from clamp.asm and build again with the second group. clamp.o still contains the function, but the name is no longer exported, so the linker cannot fill the gap in main.o and stops with a message like this one:
/usr/bin/ld.bfd: main.o: in function `mn_loop':(.text+0x34): undefined reference to `clamp'collect2: error: ld returned 1 exit statusThe first line names the file and the nearest label before the call, and 0x34 is the call's distance in bytes from the start of main.o's code. The fix goes in the file that defines the label, which is missing its .global, never in the file that uses it.
pitfall
The playground joins every file into one before it assembles, so there a label is visible everywhere whether or not it is exported, and a missing .global in a helper goes unnoticed. Only the server's linker checks. Before you hand in a program split across files, build it on the server with every file named on the gcc line.
Calling assembly from C
C and assembly follow the same calling convention, the rules for where arguments and results go, so each can call the other. A C file calls an assembly function like any other function. It needs a prototype, a line that gives the function's name, argument types, and result type without a body, and the linker finds the body in clamp.o. The C compiler puts the three arguments in w0, w1, and w2 and reads the answer from w0, exactly where clamp expects them and leaves it.
// report.c: C code that calls clamp, which is written in assembly#include <stdio.h>int clamp(int v, int lo, int hi); // the body is in clamp.asmint main(void){ int volume[] = { -3, 7, 15 }; for (int i = 0; i < 3; i++) printf("%d is set to %d\n", volume[i], clamp(volume[i], 0, 10)); return 0;}$ m4 clamp.asm > clamp.s$ gcc report.c clamp.s -o report$ ./report-3 is set to 07 is set to 715 is set to 10Calling C from assembly
The other direction needs nothing new: bl is_leap reaches a C function the same way bl printf does. The C function follows the same rules as printf. It takes its argument in w0, returns in w0, and may change x0 to x18, so the loop in years.asm keeps its values in x19 to x21 and saves them first.
// leap.c: a C function that years.asm callsint is_leap(int year){ // every fourth year, except centuries, except every fourth century return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;}// years.asm: ask a C function about four years and print each answerdefine(fp, x29)define(lr, x30)define(list_r, x19)define(k_r, w20)define(year_r, w21)save_s = 16alloc = -(16 + 32) & -16dealloc = -alloc.datafmt_leap: .string "is_leap(%d) = %d\n" .balign 4years: .word 1900, 2000, 2024, 2026.text .balign 4 .global mainmain: stp fp, lr, [sp, alloc]! mov fp, sp stp x19, x20, [fp, save_s] str x21, [fp, save_s + 16] ldr list_r, =years mov k_r, 0 b yr_testyr_loop: ldr year_r, [list_r, k_r, SXTW 2] mov w0, year_r bl is_leap // written in C, in leap.c mov w2, w0 ldr x0, =fmt_leap mov w1, year_r bl printf add k_r, k_r, 1yr_test: cmp k_r, 4 b.lt yr_loop mov w0, 0 ldp x19, x20, [fp, save_s] ldr x21, [fp, save_s + 16] ldp fp, lr, [sp], dealloc ret$ m4 years.asm > years.s$ gcc years.s leap.c -o years$ ./yearsis_leap(1900) = 0is_leap(2000) = 1is_leap(2024) = 1is_leap(2026) = 0The playground runs assembly only, so these two programs are shown here rather than run: build them on the server with the commands above.
Check yourself
- A file says
bl areaand has noarea:label. Does the assembler reject it? stats.asmdefinesmean:without.global mean, andmain.asmcallsmean. Which build step fails on the server, and which file do you fix?- Why does a helper file that uses
fpandlrneed its owndefinelines? - A C file calls
int clamp(int v, int lo, int hi). Which registers carry the three arguments and the result?
answers
show answers
- No. The assembler leaves a gap named
areain the object file; only the linker reports a name it cannot find. - The link step fails with an undefined reference to
mean. Add.global meantostats.asm, the file that defines it. - On the server
m4reads each file on its own, so names defined inmain.asmdo not exist while it reads the helper. w0,w1, andw2carry the arguments andw0the result. C uses the same calling convention as your assembly.
Practice
- Fill in the blank: external data (core): how to export a label.
- Intermediate quiz: external data: the sections these files keep their data in.
- Down to one digit: write
digsum, a function thatmaincalls. The checker takes one file, so solve it there first. Then, in the playground, movedigsumand its.global digsumline into a second file, copy thedefinelines it uses, and run the pair.