ARM assembly-language programming for the Raspberry Pi
14. More complicated string processing
In the last example I explained how to convert a binary number to a series of decimal digits for display. The process produced it's output in reverse order, so we need a way to reverse it. This example demonstrates a slightly more complicated example of string processing -- reversing a null-terminated string in memory.
The example
For brevity, I have omitted the strlen and
print_str functions, as they are unchanged from the
previous example.
// This example demonstrates how to reverse the characters of a
// null-terminated string, but swapping the characters at the ends
// and then moving inwards. This process avoids the need to allocate
// any temporary memory for the operation.
// NOTE .data, not .rodata
.section .data
EOL:
.asciz "\n"
.align 2
msg:
.asciz "Hello, World"
.text
.align 2
.global _start
/* =========================== reverse =====================================*/
// Reverse the bytes in the string whose address is in %r0. If the string is
// less than two bytes long, no change is made (but it is safe to call
// the function).
reverse:
push {r0-r5,lr}
mov %r4, %r0 // %r4 points to the start of the string
mov %r5, %r0 // So does %r5, for now
bl strlen // Get the string length in %r0
cmp %r0,$1 // Compare the length with 1
bls reverse_done // "Branch if Less or Same" to end of function
add %r5, %r0
sub %r5, $1 // %r5 points to the end of the string
lsr %r0, $1 // Divide by two
mov %r2, %r0 // %r2 now holds the loop count
reverse_loop: // Repeat the swap, working from the ends inward
ldrb %r0, [%r4] // Read the end characters into %r0 and %r1
ldrb %r1, [%r5]
mov %r3, %r1
strb %r3, [%r4] // Store the swapped end characters
strb %r0, [%r5]
add %r4, $1 // Move the pointers inwards
sub %r5, $1
sub %r2, $1 // Decrement the loop counter %r2
cmp %r2, $0 // ... and repeat if it is not yet zero
bne reverse_loop
reverse_done:
pop {r0-r5,lr}
bx lr
/* =========================== start ========================================*/
_start:
ldr %r0, =msg
bl reverse
bl print_str
ldr %r0, =EOL // Print a newline, to make the output clearer
bl print_str
// exit
mov %r0, $0
b exit
The function reverse takes the address of a null-terminated
string in register r0. It reverses the string at that
location in memory, up to the null terminator. The reversal is
'in place' -- the data supplied to the function is changed.
It would be perfectly feasible to implement a function that took
two address arguments -- one pointing to the string to reverse,
and one to an area of memory for the result. In fact, this
would be easier to implement, because the memory mangement
would be less fiddly. We could also implement a function that took
the address of a string, and allocated some new memory for the
reversed version. We haven't discussed dynamic memory allocation yet,
so that's not really feasible just now. My point is simply that
many functions lend themsevles
to a number of different modes of interaction with their callers.
We need to be a little careful not to reverse the location of the null terminator, because that would result in a string with no contents. Also, the method I've used will fail catastrophically if applied to a string that actually can't be reversed because it's too short. So the function starts by checking the length of the supplied string.
In this simple example, the string to be reversed is identified by
the label msg:. You might have noticed that I've
changed the section definition from .rodata to
.data. This is because we will be calling the
reverse function on data in this section, and the
function needs to be able to write it.
The implementation of the reverse method is, as always in
these examples, written for clarity rather than efficiency. It has
the same problem that any ARM code has, that manipulates data in
single bytes -- the same 32-bit section of memory is read repeatedly.
Even with the simplification of ignoring this fact, the function
is longer and more complicated-looking than the comparable example
in C would be. This is largely because the same construct -- the
conditional branch -- has to be used to implement the various,
more expressive
constructs that high-level languages have to control program flow.
reverse works by assigning the r4 register
to the address of the start of the string, and the r5
register to the end (not including the null). We then swap the
bytes at memory addresses r4 and r5, and
move these registers towards each other. We know how many reversal
steps are required -- half the number of characters, rounded down.
The division by two is done using a lsr (logical
shift right) operation. We'd have to be a bit careful
about doing division using a shift if the number were signed, but
the length of a string is never going to be negative.
One final new feature: I've used the .asciz tag to
define the string. This automatically adds the terminating null
to form a null-terminated string, so we don't have to remember to
add it. This adds a little readability, provided we're actually
using null-terminated strings.
Summary
Assembly programming gives rise to the same questions about the interaction between called and calling functions that affect any other kind of programming.
In the example, I chose an interface that was simple to implement, but it changed the calling function's data. This will not always be appropriate.
The availability of only a single program control structure -- the conditional branch -- makes assembly programming inexpressive, compared to high-level languages. This is a problem that can be ameliorated, but only to a limited extent, by documentation.
- Previous: 13. Elementary number formatting
- Table of contents
- Next: 15. A useable binary-to-decimal conversion
