Printing an Integer Array in (ARM64) Assembly

For the next couple tasks I want to do in assembly, I need to be able to inspect an array of numbers. This is useful for debugging searching and sorting algorithms. Since my last attempt to convert binary to ASCII was so ugly, I figured I would clean that up at the same time.

It turns out I can use the reverse code as well.

Unlike my last couple of posts where I show each of the interim steps, I have made this one work, and will just go through the final product. There was a lot of trial and error getting this to work, and I can see the need for getting organized in Assembly. The scale of the programs will quickly get beyond the scope of what I can keep in my head.

One thing this code required me to learn was how to nest function calls. I have two functions, one which calls the other. Thus, the return location for the first needs to get pushed on the stack before it calls the second, and needs to get popped off the stack at the end.

Here is the program main body. It is a function call, and a system call

_start:
 
    bl display_array
 
    /* syscall exit (int status) */
    mov    x0, #0    /* status  := 0 */
    mov    w8,  #93  /* exit is syscall #1 */
    svc    #0

I can now see how the C programming language evolved from assembler macros.

Here is the logic to convert binary to ASCII decimal. It puts into the provided buffer in reverse order, from least to most significant digit. This little endian approach means we do not have to pre-calculate the size of the buffer.

display_array:
    str LR, [SP, #16]!
 
    /*read the first value out of the array.  In the final implementation, I need to be able
      to advance this pointer value by bvalue */
 
    mov x0, #0
    ldr x6, =Array
 
loop1:
    /* this is the pointer to location where we write
       the latest value.  At the end of the loop, it points
       to the end of the text written. */
    ldr  x8, =msg  /* buf := msg */
    ldr  x7, =msg
 
    /*x2 has the result of the msub that we want to use 
       as the basis on the second and additional iterations.
       by loading the initial value into x2, we keep that logic
       at the start of the loop.  In doing so, we allow x2 to
       stay unchanged at the end of the loop, where we us it
       for the comparison. */  
    ldr w2, [x6]
    mov x1, #10
 
loop2:
 
    /* caluculate modulus of the working binary number */
    mov x0, x2
    udiv x2, x0, x1
    msub x3, x2, x1, x0
 
    /* convert binary to ascii digit  */
    add  x3, x3, #48
    strb w3, [X8]
    add x8, x8 ,#1
    cmp x2, #0
    B.NE loop2

It does mean we have to reverse the string at the end.

     /* reverse the string */
 
    ldr  x1, =msg  /* buf := msg */
    sub  x2, X8, X7  /* count = len */
    add  x2, x2, #1
    bl reverse

Append a space and null terminate the string (Probably not needed) and print out the buffer.

    mov x3, #32  /*space */
    strb w3, [X8]
    add x8, x8, #1
 
    mov x3, #0   /*null terminate the string */
    strb w3, [X8]
 
    /* calculate the string length of the number we just wrote*/
    sub x2, x8, x7
 
    /*syscall write(int fd, const void *buf, size_t count) */
    mov  x0, #1   /* fd := STDOUT_FILENO */
    ldr  x1, =msg  /* buf := msg */
 
    mov  w8, #64   /* write is syscall #64 */
    svc  #0        /*invoke syscall */

Advance the pointer to the initial buffer by 4 bytes, which is the size of a word. Make sure we don’t go off the end when we loop to the beginning.

    add x6, x6, #4
 
    ldr  x1, =Array
    ldr  x2, =ArrayLen /* count = len */
    add  x2, x1, x2
    cmp x6, x2
    B.LT loop1

After we print the buffer, print a final newline so we can compare it with the next line.

    mov  x0, #1   /* fd := STDOUT_FILENO */
    ldr  x1, =newline  /* buf := msg */
    ldr  x2, =len_newline  /* buf := msg */
    mov  w8, #64   /* write is syscall #64 */
    svc  #0        /*invoke syscall */

Restore the stack and return to the calling location.

    LDR LR, [SP], #16
    ret

In AARCH64 assembly, we have 16 General purpose registers. In order to keep from pushing and popping registers onto and off of the stack, I use a separate number of registers for the reverse code, allowing me to leave the values in the registers that are used for the numeric conversions. Thus, my reveser function now looks like this.

reverse:
 
    add X14, X1, 0    
    add X16, x1, x2
    sub X16, X16, #1
 
loop:
    sub X16, X16, #1
 
    ldrb w15, [x14] 
    ldrb w17, [x16] 
 
    strb w15, [X16]
    strb w17, [X14]
 
 
    add X14, X14, #1
    cmp X14, X16
    b.lt loop
 
    ret

I just added 10 to the register values in the first version.

This function does not call another function, so it does not need to save and restore the return value in LR.

Here is the output

503 87 61 908 170 897 709 901 271 528 834 154 231 303 435 707 424 861


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.