AArch64 Playground
4.25 · External data: .data, .bss, and .text

External data: .data, .bss, and .text

So far a variable has lived in a register or in a slot of the current function's frame, and both are gone once the function returns. This lesson covers variables that live outside every function, in memory the program keeps from the moment it starts until it ends. Because they sit outside any one function, they are called external data.

A running program's memory is split into sections: named regions that each hold one kind of thing. Three of them matter here.

  • .text holds the instructions, plus any constants that never change. The operating system marks it read-only.
  • .data holds variables that start with a value written in the source.
  • .bss holds variables that start at zero. The program file records only how big they are, and the operating system fills them with zero bytes when the program starts.

Four kinds of variable

C has four kinds of variable, told apart by where they are declared and by the word static. Each one maps onto a place in memory.

In CLives inLastsIts name can be used by
a local: int n; inside a functionthe frameone callthat function
a static local: static int n; inside a function.data or .bssthe whole runthat function
a global: int n; outside every function.data or .bssthe whole runevery function, in every file
a static global: static int n; outside every function.data or .bssthe whole runevery function in the same file

A variable written with a starting value goes in .data; one with no starting value goes in .bss, where it starts at zero. (gcc also sends a variable written as = 0 to .bss, since the result is the same.) A constant, such as const int limit = 60;, goes in .text in course programs. (gcc itself puts constants in a section called .rodata, for read-only data; the effect is the same.)

In assembly, the last three rows look alike: a label in front of some storage. The label is visible to other files only when the file says .global with its name; without that line the name stays private to its file, which is what static means for a C global. Which functions use a static local is up to you, since the assembler does not check.

A counter that survives the return

Here are two functions that should hand out numbered tickets. The first keeps its count in a static local, the second in an ordinary local:

int next_ticket(void) {    static int served = 0;      // one copy, made when the program loads    served = served + 1;    return served;}int next_ticket_frame(void) {    int count = 0;              // a new copy on every call    count = count + 1;    return count;}

In the assembly version, next_ticket keeps its counter in .data under the label served_m. Each call loads the word, adds one and stores it back, and the new value is still in memory on the next call. next_ticket_frame does the same work on a local in its frame, which is set to 0 on every entry and thrown away at ret. main calls each one three times and prints:

counter in .data: ticket 1counter in .data: ticket 2counter in .data: ticket 3counter in frame: ticket 1counter in frame: ticket 1counter in frame: ticket 1
loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

pitfall

A variable in .data is one copy shared by every call. A recursive function that keeps its working value there finds each deeper call writing over it. A value that belongs to one call goes in that call's frame.

Two ways to reach a label

Every access to external data starts by getting its address into a register.

ldr x9, =served_m is the form course programs use. The assembler stores the full 64-bit address of served_m in a literal pool, a small table of constants it places near the code, and the ldr loads the address from that table.

The other form builds the address in two instructions, without a table. Memory is divided into pages, blocks of 4096 bytes. adrp x9, label puts the address of the page that holds the label into x9, and add x9, x9, :lo12:label adds the low 12 bits of the label's address, which is its position inside the page. This is the pair gcc writes for C code. Both forms leave the same address in x9.

Three sections in one program

The next program counts how many of eight marks pass. Each piece of data sits in the section that fits it:

  • The marks start with values, so they are in .data.
  • The two counters need no starting value, so they are in .bss. .bss guarantees zero, so the program adds to them without clearing them first. .skip 4 reserves 4 bytes for each, and .balign 4 puts the first on a 4-byte boundary, the alignment a word load expects.
  • The pass mark never changes, so it is a constant in .text, reached with adrp and add. Everything else is reached with ldr =.

Five of the eight marks are 60 or more, so the program prints passed: 5, failed: 3.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

warning

.text is read-only on the servers. A store into pass_mark stops the program with a segmentation fault, the error Linux raises when a program touches memory in a way it is not allowed to. The playground does not enforce this yet, so a store into .text that runs here can still crash on the server. Keep anything you plan to change in .data or .bss.

pitfall

A common mistake from this lesson, with a broken program and its fix that you can run:

Check yourself

  1. A program needs a 4000-byte buffer that starts out empty. Which section should it go in, and what would .data cost instead?
  2. What does next_ticket_frame return on its tenth call, and why?
  3. After adrp x9, fmt_tally alone, does x9 hold the address of fmt_tally?
  4. A helper file wants to call a function named tally in this file. What line does this file need?

answers

show answers
  1. .bss. It starts at zero and takes no room in the program file; in .data the file would carry 4000 zero bytes.
  2. 1, because its counter is a frame local, made again and set to 0 on every call.
  3. Only if fmt_tally happens to start a page. adrp gives the page's address; the add with :lo12: supplies the rest.
  4. .global tally.

Practice