Finding machine language encodings

By Wolfgang Keller
Originally written 2019-06-02
Last modified 2019-10-20

Table of contents

nasm and ndisasm (x86-16, x86-32, x86-64)

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.

masm and dumpbin

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.

x86

x86-32

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

x86-64

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

ARM

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:”):

For all of these shortcuts, set “Start in:” (in German: “Ausführen in:”) to "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\".

AArch32 (T32)

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:

AArch64 (A64)

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

gas and objdump

Let us start with two remarks:

x86

x86-32

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"

x86-64

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.

ARM

AArch32 (A32, T32)

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.

A32

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]	; 10090 
   10068:	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
T32

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

AArch64 (A64)

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