HomeAboutArticles

ARM64 — print an integer in decimal

Start page1

Let's take the number x=27. Divide by 10 and get the quotient 2 and remainder 7. So 7 is the last digit and we do the same with 2. Divide by 10 gives 0 — meaning we are done — and remainder 2. So put 2 in front of 7 and we get 27.

Looking for a integer divide with remainder in the instruction set, but such a thing does not exist. We have udiv which can get the quotient, but not the remainder.

When the quotient is 2 we know 27=2*10+r, where r is the remainder we want, but then r=27-2*10, and by magic there is exactly such an instruction msub saying multiply and subtract.

udiv take 2 arguments, and below we have the 27 in x0, the 10 in x1, and the result is then found in x2.

msub takes 3 arguments, and in the code below we would have the 27 in x0, the 10 in x1, and the 2 in x2. The result is then found in x3.

So integer divide with remainder is always two instructions.

_pdec:  stp fp,lr,[sp, #-16]!
        stp x0,x1,[sp, #-16]!
        stp x2,x3,[sp, #-16]!
        mov fp,sp
        sub sp,sp,#0x20

        mov x8,fp
        mov x1,#10     /* line feed */
        strb w1,[x8]
        sub x8,x8,#1

        mov x2,x0
        mov x1,#10

loop:   mov x0, x2
        udiv x2, x0, x1        /* magic: finds remainder and */
        msub x3, x2, x1, x0    /* magic: quotient divided by 10 */

        /* convert binary to ascii digit  */
        add  x3, x3, #48
        strb w3, [X8]
        sub x8, x8 ,#1
        cmp x2, #0
        bne loop

        mov x0,1     /* print to stdout */
        mov x1,x8    /* string */
        add x1,x1,#1  /* we are one byte ahead of the start */

        sub x2,fp,x8  /* length of string */
        mov x8,0x40
        svc 0
        add sp,sp,#0x20         /* standard prologue */
        ldp x2, x3, [sp], #16
        ldp x0, x1, [sp], #16
        ldp fp, lr, [sp], #16
        ret

All the stuff around the udiv and msub are more or less standard stuff: there is a string on the stack, x8 points to the address of the next character in the string, running backwards. And we start by putting a newline at the end of the string.

You should put this code in say lib4.s &mdash as described before.

The call it a few times, e.g.:

mov x0,7913
bl _pdec

mov x0,0xFFFFFFFFFFFFFFFF
bl _pdec

add x0,x0,1
bl _pdec

add x0,x0,1
bl _pdec

This should print 7913, followed by the largest unsigned 64-bit integer possible. Adding 1 to that then gives 0, and adding 1 again gives 1.

By the way, that is a surprise: we can't just put a 64-bit address into x0 in a single instruction. So the assembler is cheating us? Yeah, go find out how. Turns out it sees this is as -1, and knows how.

mov x0,0xFFCFFCFFCFFCFFCF

gives an Error: immediate cannot be moved by a single instruction

But we can always:

         ldr x0,bignum

and then somewhere close:

bignum:  .dword 0xFFCFFCFFCFFCFFCF

How do we handle signed 64-bit integers. Don't ask me, as I don't know. Yet.

Start page1

2025-07-05