By Wolfgang Keller
Originally published 2019-06-02
Last modified 2019-10-20
Using nasm, finding machine language encoding is very easy. Create a file, let's name it nasm_32.asm:
bits 32
mov eax, 12345678h
nop
ret
If you want to create 16 or 64 bit assembly code, change the line bits 32
appropriately.
Assemble it via nasm nasm_32.asm. This creates a file called nasm_32. Now disassemble it via ndisasm -b 32 nasm_32:
00000000 B878563412 mov eax,0x12345678
00000005 90 nop
00000006 C3 ret
If you want to disassemble 16 or 64 bit machine code, change the parameter -b 32 appropriately.
At Finding Machine Language Encodings [published 2017-02-15; visited 2019-05-31T11:00:51Z], one can find a tutorial how to use masm and dumpbin to find machine language encodings of x86 assembly language instructions. The following explanation is based on this source, complemented by an own explanation for the 64 bit version.
We first remark that masm and dumpbin do not support 16 bit machine code - only 32 and 64 bit.
Either run “x86 Native Tools Command Prompt for VS 2019” or “x64_x86 Cross Tools Command Prompt for VS 2019”.
Create a file, let's name it masm_x86-32.asm:
.model flat
.code
example PROC
mov eax, 12345678h
nop
ret
example ENDP
END
Compile it via ml /c masm_x86-32.asm. This creates a file called masm_x86-32.obj. Now disassemble it via dumpbin /DISASM masm_x86-32.obj:
Microsoft (R) COFF/PE Dumper Version 14.21.27702.2
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file masm_x86-32.obj
File Type: COFF OBJECT
example:
00000000: B8 78 56 34 12 mov eax,12345678h
00000005: 90 nop
00000006: C3 ret
Summary
0 .data
88 .debug$S
7 .text$mn
Either run “x64 Native Tools Command Prompt for VS 2019” or “x86_x64 Cross Tools Command Prompt for VS 2019”.
Create a file, let's name it masm_x86-64.asm:
.code
example PROC
mov eax, 12345678h
nop
ret
example ENDP
END
Compile it via ml64 /c masm_x86-64.asm. This creates a file called masm_x86-64.obj. Now disassemble it via dumpbin /DISASM masm_x86-64.obj:
Microsoft (R) COFF/PE Dumper Version 14.21.27702.2
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file masm_x86-64.obj
File Type: COFF OBJECT
example:
0000000000000000: B8 78 56 34 12 mov eax,12345678h
0000000000000005: 90 nop
0000000000000006: C3 ret
Summary
0 .data
88 .debug$S
7 .text$mn
The build tools for ARM and ARM64 are not installed by default by the Visual Studio Installer. So, you have to install them manually.
A worse problem is that (in all likelihood because of a bug) is that the Visual Studio Installer does not create a shortcut for the Tools Command Prompt. Thus, you have to do it on your own. For this, create sortcuts (if you want, you can, as an adminstrator, put these into C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2019\Visual Studio Tools\VC) with the following value for “Target:” (in German: “Ziel:”):
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsamd64_arm64.bat"
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsamd64_arm.bat"
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsx86_arm64.bat"
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsx86_arm.bat"
For all of these shortcuts, set “Start in:” (in German: “Ausführen in:”) to
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\"
.
Run the shortcut to vcvarsamd64_arm.bat or vcvarsx86_arm.bat.
Create a file, let's name it masm_ARM-T32.asm:
; the THUMB attribute is redundant
AREA my_test1, CODE, THUMB, READONLY
EXPORT test1
test1 PROC
mov r0, #0xC
dcw 0xBF00
nop
bx lr
ENDP
END
Compile it via armasm masm_ARM-T32.asm. This creates a file called masm_ARM-T32.obj. Now disassemble it via dumpbin /DISASM masm_ARM-T32.obj:
Microsoft (R) COFF/PE Dumper Version 14.22.27905.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file masm_ARM-T32.obj File Type: COFF OBJECT test1: 00000000: F04F 000C mov r0,#0xC 00000004: BF00 nop 00000006: BF00 nop 00000008: 4770 bx lr Summary BC .debug$S A my_test1
You now probably ask: is it also possible to generate and disassemble A32 machine code? The answer is:
Nevertheless, here are two files for tinkering:
masm_ARM-A32.asm, which contains only A32 code:
AREA my_test2, CODE, ARM, READONLY
EXPORT test2
ARM
test2 PROC
mov r0, #0xC
nop
bx lr
ENDP
END
masm_ARM-A32-T32.asm, which contains both A32 and T32 code:
; the THUMB attribute is redundant
AREA my_test1, CODE, THUMB, READONLY
EXPORT test1
test1 PROC
mov r0, #0xC
dcw 0xBF00
nop
bx lr
ENDP
AREA my_test2, CODE, ARM, READONLY
EXPORT test2
ARM
test2 PROC
mov r0, #0xC
nop
bx lr
ENDP
END
Run the shortcut to Start vcvarsamd64_arm64.bat or vcvarsx86_arm64.bat.
Create a file, let's name it masm_ARM-A64.asm:
AREA my_test1, CODE, READONLY
EXPORT test1
test1 PROC
mov x0, #0xC
nop
ret
ENDP
END
Compile it via armasm64 masm_ARM-A64.asm. This creates a file called masm_ARM-A64.obj. Now disassemble it via dumpbin /DISASM masm_ARM-A64.obj:
Microsoft (R) COFF/PE Dumper Version 14.22.27905.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file masm_ARM-A64.obj File Type: COFF OBJECT test1: 0000000000000000: D2800180 mov x0,#0xC 0000000000000004: D503201F nop 0000000000000008: D65F03C0 ret Summary BC .debug$S C my_test1
Let us start with two remarks:
In this section, we only explain the basic ideas. A much more comprehensive explanation can be found in the section about gas and x86-64 (section ).
Install the respective binutils via sudo apt-get install binutils-i686-linux-gnu.
The following example is inspired by https://riptutorial.com/x86/example/19078/x86-linux-hello-world-example.
Create a file, let's name it gas_x86-32.s:
.global _start
.text
_start:
# write(1, message, 13)
mov $4, %eax # system call 4 is write
mov $1, %ebx # file handle 1 is stdout
mov $message, %ecx # address of string to output
mov $13, %edx # number of bytes
int $0x80 # invoke operating system to do the write
# exit(0)
mov $1, %eax # system call 1 is exit
xor %ebx, %ebx # we want return code 0
int $0x80 # invoke operating system to exit
# For testing
.byte 0xCD, 0x80
message:
.ascii "Hello, world\n"
Assemble and link this program via
i686-linux-gnu-as gas_x86-32.s -o gas_x86-32.o
i686-linux-gnu-ld gas_x86-32.o -o gas_x86-32
Now run i686-linux-gnu-objdump -d gas_x86-32:
gas_x86-32.o: Dateiformat elf32-i386 Disassembly of section .text: 00000000 <_start>: 0: b8 04 00 00 00 mov $0x4,%eax 5: bb 01 00 00 00 mov $0x1,%ebx a: b9 21 00 00 00 mov $0x21,%ecx f: ba 0d 00 00 00 mov $0xd,%edx 14: cd 80 int $0x80 16: b8 01 00 00 00 mov $0x1,%eax 1b: 31 db xor %ebx,%ebx 1d: cd 80 int $0x80 1f: cd 80 int $0x80 00000021 <message>: 21: 48 dec %eax 22: 65 6c gs insb (%dx),%es:(%edi) 24: 6c insb (%dx),%es:(%edi) 25: 6f outsl %ds:(%esi),(%dx) 26: 2c 20 sub $0x20,%al 28: 77 6f ja 99 <message+0x78> 2a: 72 6c jb 98 <message+0x77> 2c: 64 fs 2d: 0a .byte 0xa
If you prefer the Intel syntax, use i686-linux-gnu-objdump -M intel -d gas_x86-32:
gas_x86-32: Dateiformat elf32-i386 Disassembly of section .text: 08048054 <_start>: 8048054: b8 04 00 00 00 mov eax,0x4 8048059: bb 01 00 00 00 mov ebx,0x1 804805e: b9 75 80 04 08 mov ecx,0x8048075 8048063: ba 0d 00 00 00 mov edx,0xd 8048068: cd 80 int 0x80 804806a: b8 01 00 00 00 mov eax,0x1 804806f: 31 db xor ebx,ebx 8048071: cd 80 int 0x80 8048073: cd 80 int 0x80 08048075 <message>: 8048075: 48 dec eax 8048076: 65 6c gs ins BYTE PTR es:[edi],dx 8048078: 6c ins BYTE PTR es:[edi],dx 8048079: 6f outs dx,DWORD PTR ds:[esi] 804807a: 2c 20 sub al,0x20 804807c: 77 6f ja 80480ed <message+0x78> 804807e: 72 6c jb 80480ec <message+0x77> 8048080: 64 fs 8048081: 0a .byte 0xa
For the sake of completeness, a version that puts the string into a .rodata
section (gas_x86-32_sections.s):
.global _start
.text
_start:
# write(1, message, 13)
mov $4, %eax # system call 4 is write
mov $1, %ebx # file handle 1 is stdout
mov $message, %ecx # address of string to output
mov $13, %edx # number of bytes
int $0x80 # invoke operating system to do the write
# exit(0)
mov $1, %eax # system call 1 is exit
xor %ebx, %ebx # we want return code 0
int $0x80 # invoke operating system to exit
# For testing
.byte 0xCD, 0x80
.section .rodata
message:
.ascii "Hello, world\n"
Install the respective binutils via sudo apt-get install binutils-x86-64-linux-gnu.
The following example is inspired by the first example from https://cs.lmu.edu/~ray/notes/gasexamples/ [visited 2019-09-29T18:06:49Z].
Create a file, let's name it gas_x86-64.s:
.global _start
.text
_start:
# write(1, message, 13)
mov $1, %rax # system call 1 is write
mov $1, %rdi # file handle 1 is stdout
mov $message, %rsi # address of string to output
mov $13, %rdx # number of bytes
syscall # invoke operating system to do the write
# exit(0)
mov $60, %rax # system call 60 is exit
xor %rdi, %rdi # we want return code 0
syscall # invoke operating system to exit
# For testing
.byte 0x0F, 0x05
message:
.ascii "Hello, world\n"
Assemble it via x86_64-linux-gnu-as gas_x86-64.s -o gas_x86-64.o. You might now be tempted to run x86_64-linux-gnu-objdump -d gas_x86-64.o. But this will yield an inorrect result:
gas_x86-64.o: Dateiformat elf64-x86-64 Disassembly of section .text: 0000000000000000 <_start>: 0: 48 c7 c0 01 00 00 00 mov $0x1,%rax 7: 48 c7 c7 01 00 00 00 mov $0x1,%rdi e: 48 c7 c6 00 00 00 00 mov $0x0,%rsi 15: 48 c7 c2 0d 00 00 00 mov $0xd,%rdx 1c: 0f 05 syscall 1e: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 25: 48 31 ff xor %rdi,%rdi 28: 0f 05 syscall 2a: 0f 05 syscall 000000000000002c <message>: 2c: 48 rex.W 2d: 65 6c gs insb (%dx),%es:(%rdi) 2f: 6c insb (%dx),%es:(%rdi) 30: 6f outsl %ds:(%rsi),(%dx) 31: 2c 20 sub $0x20,%al 33: 77 6f ja a4 <message+0x78> 35: 72 6c jb a3 <message+0x77> 37: 64 fs 38: 0a .byte 0xa
The reason should be obvious: the label message
has not yet been resolved,
so at the respective place (offset 0x11-0x14), gas
puts zeros.
We remark that other assemblers than gas resolve labels that
are defined in the same file automatically.
So, as a next step, we have to link the generated object file. Run x86_64-linux-gnu-ld gas_x86-64.o -o gas_x86-64. Then run x86_64-linux-gnu-objdump -d gas_x86-64 and obtain
gas_x86-64: Dateiformat elf64-x86-64 Disassembly of section .text: 0000000000400078 <_start>: 400078: 48 c7 c0 01 00 00 00 mov $0x1,%rax 40007f: 48 c7 c7 01 00 00 00 mov $0x1,%rdi 400086: 48 c7 c6 a4 00 40 00 mov $0x4000a4,%rsi 40008d: 48 c7 c2 0d 00 00 00 mov $0xd,%rdx 400094: 0f 05 syscall 400096: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 40009d: 48 31 ff xor %rdi,%rdi 4000a0: 0f 05 syscall 4000a2: 0f 05 syscall 00000000004000a4 <message>: 4000a4: 48 rex.W 4000a5: 65 6c gs insb (%dx),%es:(%rdi) 4000a7: 6c insb (%dx),%es:(%rdi) 4000a8: 6f outsl %ds:(%rsi),(%dx) 4000a9: 2c 20 sub $0x20,%al 4000ab: 77 6f ja 40011c <message+0x78> 4000ad: 72 6c jb 40011b <message+0x77> 4000af: 64 fs 4000b0: 0a .byte 0xa
Of course, the second part of the output makes no sense; you would normally put such data in an extra
readonly data section (.rodata
).
If you prefer the Intel syntax, use x86_64-linux-gnu-objdump -M intel -d gas_x86-64:
gas_x86-64: Dateiformat elf64-x86-64 Disassembly of section .text: 0000000000400078 <_start>: 400078: 48 c7 c0 01 00 00 00 mov rax,0x1 40007f: 48 c7 c7 01 00 00 00 mov rdi,0x1 400086: 48 c7 c6 a4 00 40 00 mov rsi,0x4000a4 40008d: 48 c7 c2 0d 00 00 00 mov rdx,0xd 400094: 0f 05 syscall 400096: 48 c7 c0 3c 00 00 00 mov rax,0x3c 40009d: 48 31 ff xor rdi,rdi 4000a0: 0f 05 syscall 4000a2: 0f 05 syscall 00000000004000a4 <message>: 4000a4: 48 rex.W 4000a5: 65 6c gs ins BYTE PTR es:[rdi],dx 4000a7: 6c ins BYTE PTR es:[rdi],dx 4000a8: 6f outs dx,DWORD PTR ds:[rsi] 4000a9: 2c 20 sub al,0x20 4000ab: 77 6f ja 40011c <message+0x78> 4000ad: 72 6c jb 40011b <message+0x77> 4000af: 64 fs 4000b0: 0a .byte 0xa
If you run an x86-64 GNU/Linux system, you can excute this program via ./gas_x86-64.
Above, we remarked that you would normally put the string into a readonly data section (.rodata
).
For teaching purposes, let us consider such a program; let us name it
gas_x86-64_sections.s:
.global _start
.text
_start:
# write(1, message, 13)
mov $1, %rax # system call 1 is write
mov $1, %rdi # file handle 1 is stdout
mov $message, %rsi # address of string to output
mov $13, %rdx # number of bytes
syscall # invoke operating system to do the write
# exit(0)
mov $60, %rax # system call 60 is exit
xor %rdi, %rdi # we want return code 0
syscall # invoke operating system to exit
# For testing
.byte 0x0F, 0x05
.section .rodata
message:
.ascii "Hello, world\n"
Similarly to before, you can assemble and link this program via
x86_64-linux-gnu-as gas_x86-64_sections.s -o gas_x86-64_sections.o
x86_64-linux-gnu-ld gas_x86-64_sections.o -o gas_x86-64_sections
(and if you run an x86-64 GNU/Linux system, you can excute it program via ./gas_x86-64_sections).
Where is the problem? Consider the output of x86_64-linux-gnu-objdump -d gas_x86-64_sections:
gas_x86-64_sections: Dateiformat elf64-x86-64
Disassembly of section .text:
0000000000400078 <_start>:
400078: 48 c7 c0 01 00 00 00 mov $0x1,%rax
40007f: 48 c7 c7 01 00 00 00 mov $0x1,%rdi
400086: 48 c7 c6 a4 00 40 00 mov $0x4000a4,%rsi
40008d: 48 c7 c2 0d 00 00 00 mov $0xd,%rdx
400094: 0f 05 syscall
400096: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax
40009d: 48 31 ff xor %rdi,%rdi
4000a0: 0f 05 syscall
4000a2: 0f 05 syscall
I.e. the “Hello, world\n” string is not shown.
In this section, we only explain the basic ideas. A much more comprehensive explanation can be found in the section about gas and x86-64 (section ).
Install the respective binutils via sudo apt-get install binutils-arm-linux-gnueabihf.
The following example is inspired by https://peterdn.com/post/2019/02/03/hello-world-in-arm-assembly/.
Create a file, let's name it gas_A32.s:
.text
.globl _start
_start:
/* syscall write(int fd, const void *buf, size_t count) */
mov %r0, $1 /* fd := STDOUT_FILENO */
ldr %r1, =msg /* buf := msg */
mov %r2, $13 /* count := 13 */
mov %r7, $4 /* write is syscall #4 */
swi $0 /* invoke syscall */
/* syscall exit(int status) */
mov %r0, $0 /* status := 0 */
mov %r7, $1 /* exit is syscall #1 */
swi $0 /* invoke syscall */
.align 4
msg:
.ascii "Hello, world\n"
Assemble and link this program via
arm-linux-gnueabihf-as gas_A32.s -o gas_A32.o
arm-linux-gnueabihf-ld gas_A32.o -o gas_A32
Now run arm-linux-gnueabihf-objdump -d gas_A32:
gas_A32: Dateiformat elf32-littlearm Disassembly of section .text: 00010060 <_start>: 10060: e3a00001 mov r0, #1 10064: e59f1024 ldr r1, [pc, #36] ; 1009010068: e3a0200d mov r2, #13 1006c: e3a07004 mov r7, #4 10070: ef000000 svc 0x00000000 10074: e3a00000 mov r0, #0 10078: e3a07001 mov r7, #1 1007c: ef000000 svc 0x00000000 00010080 <msg>: 10080: 6c6c6548 .word 0x6c6c6548 10084: 77202c6f .word 0x77202c6f 10088: 646c726f .word 0x646c726f 1008c: 0000000a .word 0x0000000a 10090: 00010080 .word 0x00010080
If you run a GNU/Linux system that uses the gnueabihf ABI (for example (32 bit) Raspbian on a Raspberry Pi), you can excute this program via ./gas_A32.
For the sake of completeness, a version that puts the string into a .data
section (gas_A32_sections.s):
.text
.globl _start
_start:
/* syscall write(int fd, const void *buf, size_t count) */
mov %r0, $1 /* fd := STDOUT_FILENO */
ldr %r1, =msg /* buf := msg */
ldr %r2, =len /* count := len */
mov %r7, $4 /* write is syscall #4 */
swi $0 /* invoke syscall */
/* syscall exit(int status) */
mov %r0, $0 /* status := 0 */
mov %r7, $1 /* exit is syscall #1 */
swi $0 /* invoke syscall */
.data
msg:
.ascii "Hello, world\n"
len = . - msg
The following example is a conversion of the example that we presented in the section about gas/A32 (section ) to T32.
Create a file, let's name it gas_T32.s. We highlighted the additions over gas_A32.s:
.text
.code 16
.globl _start
.thumb_func
_start:
/* syscall write(int fd, const void *buf, size_t count) */
mov %r0, $1 /* fd := STDOUT_FILENO */
ldr %r1, =msg /* buf := msg */
mov %r2, $13 /* count := 13 */
mov %r7, $4 /* write is syscall #4 */
swi $0 /* invoke syscall */
/* syscall exit(int status) */
mov %r0, $0 /* status := 0 */
mov %r7, $1 /* exit is syscall #1 */
swi $0 /* invoke syscall */
.align 4
msg:
.ascii "Hello, world\n"
Assemble and link this program via
arm-linux-gnueabihf-as gas_T32.s -o gas_T32.o
arm-linux-gnueabihf-ld gas_T32.o -o gas_T32
Now run arm-linux-gnueabihf-objdump -d gas_T32:
gas_T32: file format elf32-littlearm Disassembly of section .text: 00010060 <_start>: 10060: 2001 movs r0, #1 10062: 4907 ldr r1, [pc, #28] ; (10080 <msg+0x10>) 10064: 220d movs r2, #13 10066: 2704 movs r7, #4 10068: df00 svc 0 1006a: 2000 movs r0, #0 1006c: 2701 movs r7, #1 1006e: df00 svc 0 00010070 <msg>: 10070: 6c6c6548 .word 0x6c6c6548 10074: 77202c6f .word 0x77202c6f 10078: 646c726f .word 0x646c726f 1007c: 0000000a .word 0x0000000a 10080: 00010070 .word 0x00010070
If you run a GNU/Linux system that uses the gnueabihf ABI (for example (32 bit) Raspbian on a Raspberry Pi), you can excute this program via ./gas_T32.
For the sake of completeness, a version that puts the string into a .data
section
(gas_T32_sections.s).
Also here, we highlighted the
additions over gas_A32_sections.s:
.text
.code 16
.globl _start
.thumb_func
_start:
/* syscall write(int fd, const void *buf, size_t count) */
mov %r0, $1 /* fd := STDOUT_FILENO */
ldr %r1, =msg /* buf := msg */
ldr %r2, =len /* count := len */
mov %r7, $4 /* write is syscall #4 */
swi $0 /* invoke syscall */
/* syscall exit(int status) */
mov %r0, $0 /* status := 0 */
mov %r7, $1 /* exit is syscall #1 */
swi $0 /* invoke syscall */
.data
msg:
.ascii "Hello, world\n"
len = . - msg
In this section, we only explain the basic ideas. A much more comprehensive explanation can be found in the section about gas and x86-64 (section ).
Install the respective binutils via sudo apt-get install binutils-aarch64-linux-gnu.
The following example is inspired by https://stackoverflow.com/questions/6242416/tools-required-to-learn-arm-on-linux-x86-platform/54461673#54461673.
Create a file, let's name it gas_A64.s:
.text
.globl _start
_start:
/* syscall write(int fd, const void *buf, size_t count) */
mov x0, #1 /* fd := STDOUT_FILENO */
adr x1, msg /* buf := msg */
mov x2, 13 /* count := 13 */
mov x8, #64 /* write is syscall #64 */
svc #0 /* invoke syscall */
/* syscall exit(int status) */
mov x0, #0 /* status := 0 */
mov x8, #93 /* exit is syscall #93 */
svc #0 /* invoke syscall */
msg:
.ascii "Hello, world\n"
Assemble and link this program via
aarch64-linux-gnu-as gas_A64.s -o gas_A64.o
aarch64-linux-gnu-ld gas_A64.o -o gas_A64
Now run aarch64-linux-gnu-objdump -d gas_A64:
gas_A64: Dateiformat elf64-littleaarch64 Disassembly of section .text: 0000000000400078 <_start>: 400078: d2800020 mov x0, #0x1 // #1 40007c: 100000e1 adr x1, 400098 <msg> 400080: d28001a2 mov x2, #0xd // #13 400084: d2800808 mov x8, #0x40 // #64 400088: d4000001 svc #0x0 40008c: d2800000 mov x0, #0x0 // #0 400090: d2800ba8 mov x8, #0x5d // #93 400094: d4000001 svc #0x0 0000000000400098 <msg>: 400098: 6c6c6548 .word 0x6c6c6548 40009c: 77202c6f .word 0x77202c6f 4000a0: 646c726f .word 0x646c726f 4000a4: Adresse 0x00000000004000a4 ist außerhalb des gültigen Bereichs.
For the sake of completeness, a version that puts the string into a .data
section (gas_A64_sections.s):
.text
.globl _start
_start:
/* syscall write(int fd, const void *buf, size_t count) */
mov x0, #1 /* fd := STDOUT_FILENO */
adr x1, msg /* buf := msg */
mov x2, len /* count := len */
mov x8, #64 /* write is syscall #64 */
svc #0 /* invoke syscall */
/* syscall exit(int status) */
mov x0, #0 /* status := 0 */
mov x8, #93 /* exit is syscall #93 */
svc #0 /* invoke syscall */
.data
msg:
.ascii "Hello, world\n"
len = . - msg