Important notice about this site

I graduated from the University of Texas at Austin in December 2020. This blog is archived and no updates will be made to it.

Emulating 'Hello World' in ARM

May 15, 2019

Who knew "Hello world!" would be so difficult to emulate?

For my Computer Architecture class, we got to pick our final project. Three classmates and I decided to group up and extend the ARM AArch64 emulator we created earlier in the semester in the class so it could support the printf function in C.

Unfortunately, this was much easier said than done.

To understand the difficulty behind emulating printf, let's explore what C has to do with our ARM emulator. We would write a C program that was compiled without the C standard library included, so it would convert the C code that we wrote into AArch64 assembly instructions. However, real programs run with the C standard library, which itself is a lot of assembly instructions.

For starters, we would have to emulate all of the instructions used by the C standard library to start, before it could even begin to execute whatever is in the main() function. It turns out there is a mind-boggling amount of work that the GNU C library does for each program in order for it to prepare to do whatever awaits it in main(). That's hundreds of instructions, some of which were SIMD/vector instructions that we did not even know about originally.

The real kicker is that we have to emulate syscalls. Whenever the assembly file says svc #0x0, that means we have to take whatever value is stored in x8 and look up which syscall to perform as specified in the syscall table that the OS (in this case, Linux) provides. The emulator therefore has to trick the program into thinking that the syscall executed correctly and that the expected return value is provided.

Eventually, we didn't make much progress on emulating the GNU C library's startup functions, so we switched over to the musl C library. It featured much fewer instructions than the GNU C library used, and didn't make as many pointless syscalls (such as calling brk for a simple hello world printf program - why does the heap need to be expanded for that?). Unfortunately, we still didn't make too much progress on emulating the entire musl startup process either.

At the end of the day, we realized just how much work was necessary in order to implement the C standard library, whether the GNU implementation or musl. That being said, we sure did learn a lot about syscalls and other tidbits about the ARMv8 architecture. While we couldn't get "Hello world" to print out with the C standard library, we were at least able to emulate a simple hello world assembly program that used syscalls to make this happen. Overall, extending our ARMv8 Emulator to support the C standard library was a somewhat disappointing yet very insightful experience.

Here is the disassembly of the simple hello world assembly program that makes syscalls directly without the overhead of the C standard library. Actually, write_char at 400174 is slightly inaccurate; it is missing a ret statement. In reality, it should include one; I am just too lazy to regenerate the .disas again to get it.

writesyscall:     file format elf64-littleaarch64

Disassembly of section .note.gnu.build-id:

00000000004000e8 <.note.gnu.build-id>:
  4000e8:   00000004    .inst   0x00000004 ; undefined
  4000ec:   00000014    .inst   0x00000014 ; undefined
  4000f0:   00000003    .inst   0x00000003 ; undefined
  4000f4:   00554e47    .inst   0x00554e47 ; undefined
  4000f8:   6ec8f7ae    .inst   0x6ec8f7ae ; undefined
  4000fc:   ab44b93d    adds    x29, x9, x4, lsr #46
  400100:   4d434f15    .inst   0x4d434f15 ; undefined
  400104:   410d9a08    .inst   0x410d9a08 ; undefined
  400108:   cce822a0    .inst   0xcce822a0 ; undefined

Disassembly of section .text:

000000000040010c <write_string>:
  40010c:   a9bd7bfd    stp x29, x30, [sp,#-48]!
  400110:   910003fd    mov x29, sp
  400114:   f9000fa0    str x0, [x29,#24]
  400118:   f9400fa0    ldr x0, [x29,#24]
  40011c:   39400000    ldrb    w0, [x0]
  400120:   3900bfa0    strb    w0, [x29,#47]
  400124:   3940bfa0    ldrb    w0, [x29,#47]
  400128:   7100001f    cmp w0, #0x0
  40012c:   540000e0    b.eq    400148 <write_string+0x3c>
  400130:   f9400fa0    ldr x0, [x29,#24]
  400134:   94000010    bl  400174 <write_char>
  400138:   f9400fa0    ldr x0, [x29,#24]
  40013c:   91000400    add x0, x0, #0x1
  400140:   f9000fa0    str x0, [x29,#24]
  400144:   17fffff5    b   400118 <write_string+0xc>
  400148:   d503201f    nop
  40014c:   a8c37bfd    ldp x29, x30, [sp],#48
  400150:   d65f03c0    ret

0000000000400154 <start>:
  400154:   a9bf7bfd    stp x29, x30, [sp,#-16]!
  400158:   910003fd    mov x29, sp
  40015c:   90000000    adrp    x0, 400000 <write_string-0x10c>
  400160:   91062000    add x0, x0, #0x188
  400164:   97ffffea    bl  40010c <write_string>
  400168:   d503201f    nop
  40016c:   a8c17bfd    ldp x29, x30, [sp],#16
  400170:   d65f03c0    ret

0000000000400174 <write_char>:
  400174:   d2800808    mov x8, #0x40                   // #64
  400178:   aa0003e1    mov x1, x0
  40017c:   d2800020    mov x0, #0x1                    // #1
  400180:   d2800022    mov x2, #0x1                    // #1
  400184:   d4000001    svc #0x0

Disassembly of section .rodata:

0000000000400188 <__bss_end__-0x10007>:
  400188:   6c6c6568    .word   0x6c6c6568
  40018c:   Address 0x000000000040018c is out of bounds.

Disassembly of section .comment:

0000000000000000 <.comment>:
   0:   3a434347    ccmn    w26, w3, #0x7, mi
   4:   694c2820    ldpsw   x0, x10, [x1,#96]
   8:   6f72616e    umlsl2  v14.4s, v11.8h, v2.h[3]
   c:   43434720    .inst   0x43434720 ; undefined
  10:   352e3520    cbnz    w0, 5c6b4 <write_string-0x3a3a58>
  14:   3130322d    adds    w13, w17, #0xc0c
  18:   30312e37    adr x23, 625dd <write_string-0x39db2f>
  1c:   2e352029    usubl   v9.8h, v1.8b, v21.8b
  20:   00302e35    .inst   0x00302e35 ; NYI

The binary was created by compiling and linking writesyscall.c and write_char.S.

writesyscall.c:

extern void write_char(const char* c);

void write_string(const char* s) {
    do {
        char c = *s;
        if (c == 0) return;
        write_char(s);
        s++;
    } while(1);
}

void _start() {
    write_string("Hello world!\n");
}

write_char.S:

.global write_char

write_char:
mov x8, #0x40
mov x1, x0
mov x0, #1
mov x2, #1
svc #0x0
ret

Back to blog

Back to top