Printing a variable from .data
prerequisite
Read these first:
In many assembly programs, data is stored in variables in the .data section before it is used. Printing an immediate, a constant written into the instruction itself, is straightforward. Printing a value stored in memory takes an extra step, because the value has to be loaded into a register first.
This lesson loads a value out of .data into a register, passes it to printf, and then changes the value in memory.
A review of printing with printf
Printing in assembly always follows the same four steps:
- Define the string or format string: the text to print, with a specifier such as
%dmarking where each value goes. - Load the address of the string or format string into
x0. - Place each value to be printed in the next argument register (
w1orx1, thenw2orx2, and so on), if there are any. - Call
printf.
These steps stay the same when printing variables stored in the .data section. The difference is in step 3.
In previous lessons, the values being printed were immediates, moved straight into an argument register. For example:
mov w1, 67
Here the value comes out of a .data variable instead of an immediate, but it still ends up in the same argument register.
Loading values from .data
Suppose we have an integer value of 85 stored in a variable named importantNumber in the .data section, as shown below:
.dataimportantNumber: .word 85 // a 4-byte integerTo load the value stored in importantNumber, we must first get the address of the variable. Once we have the address, we can use it to load the value stored at that memory location. The following two lines do both:
ldr x9, =importantNumber // x9 = the address of the variable ldr w1, [x9] // w1 = the value stored at that addressThe ldr instruction is used both to get the address of the variable and to load the value stored at that address, so two ldr instructions are needed.
The first ldr puts the address of the variable in register x9. The = in front of the label asks for the label's address rather than what is stored there, and an address is 64 bits, so it always goes in an x register. The second ldr uses this address to reach memory and loads the value found there into register w1. A .word is 32 bits, so the load uses the 32-bit w form of the register; an x1 load would drag in the 4 bytes stored after the variable too. The register x0 is reserved for the address of the format string, which is passed to printf as the first argument.
x9 is a scratch register: a function you call, such as printf, is allowed to change it. That is fine here, because the address is needed only until the value is loaded.
note
The square brackets [] in the ldr instruction indicate memory access (dereferencing). They tell the processor to use the value inside the register as a memory address and access the data stored at that location.
Printing the variable
Once the format string address is in x0 and the value is in w1, printf can be called to display the output. The complete program below prints Integer Variable Content: 85.
note
Notice the .balign 4 line between the string and the word. .balign 4 adds zero bytes until the next address is a multiple of 4. The string takes 30 bytes (29 characters plus the zero byte that marks its end), so without the padding the word would start 30 bytes into .data, which is not a multiple of 4. A 4-byte value belongs at an address divisible by 4, and .balign 4 makes sure of that however long the string above it is.
Changing a variable in memory
str (store register) is the mirror image of ldr: it copies a register into memory at the address in the square brackets. Loading a variable gives you a copy in a register, and changing the copy leaves the variable in memory as it was. To change the variable itself, store the new value back to its address.
The program below loads a counter that starts at 41, adds 1 to the copy, stores the copy back, and then loads the counter again. It prints visits before: 41, after: 42. Delete the str line and run it again: the second number stays 41, because only the register copy changed.
note
You can print variables of any data type using the process shown above. Use the format specifier and the register width that match the variable's type.
When calling printf, you must pass arguments whose data types match their corresponding format specifiers. A mismatch prints garbage.
pitfall
A common mistake from this lesson, with a broken program and its fix that you can run:
Check yourself
- Why does printing a
.datavariable take twoldrinstructions instead of one? countis a.wordprinted with%d. Should it be loaded intow1orx1, and why?- After
ldr w10, [x9]andadd w10, w10, 5, has the variable at the address inx9changed? Which instruction would change it?
answers
show answers
- The first
ldrgets the variable's address, and the second loads the value stored at that address. w1: a.wordis 4 bytes and%dprints a 32-bitint, while anx1load would pull in the 4 bytes after the variable as well.- No, only the copy in
w10changed.str w10, [x9]writes it back.
Practice
- The price is right: load two words from
.data, multiply them, and store the product in a third. - Sum a global array: add up an array of scores that lives in
.bss. It needs a loop, so try it after the pre-test loop lesson. - Fill in the blank: external data (core): the directives that declare data, such as
.dwordfor a 64-bit variable and.stringfor text. Its.skipquestion comes with the external data lesson.