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.
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 top