Dynasm syscall hello world help?

use dynasmrt::{DynasmApi, DynasmLabelApi};

fn main() {
    let mut ops = dynasmrt::x64::Assembler::new().unwrap();
    let hello_world = "hello, world!";

    dynasm!(ops
        ; .arch x64
        ; ->hello:
        ; .bytes hello_world.as_bytes()
    );

    println!("before");

    let hello = ops.offset();
    dynasm!(ops
        ; .arch x64

        ; mov rax, 1
        ; mov rdi, 1
        ; mov rsi, [-> hello]
        ; mov rdx, 12
        ; syscall

        ; ret


    );

    println!("after");

    let buf = ops.finalize().unwrap();

    let hello_fn: extern "sysv64" fn() = unsafe { std::mem::transmute(buf.ptr(hello)) };

    hello_fn();}

actual output:

before
after

expected output: a 'hello world' between the before/after

dynasm!() only appends instructions to the ops buffer. You need to add a prologue and epilogue first matching the calling convention you want, use ops.finalize() to get an executable buffer and then call the assembled function using the right calling convention.

Is

fn main() {
    let mut ops = dynasmrt::x64::Assembler::new().unwrap();
    let hello_world = "hello, world!\n\0"; 
    dynasm!(ops
        ; .arch x64
        ; ->hello:
        ; .bytes hello_world.as_bytes()
    ); 
    let hello = ops.offset();
    dynasm!(ops
        ; .arch x64
        ; mov rax, 1
        ; mov rdi, 1
        ; mov rsi, [-> hello]
        ; mov rdx, hello_world.len() as i32
        ; syscall
        ; ret
    ); 
    let buf = ops.finalize().unwrap(); 
    let hello_fn: extern "sysv64" fn() = unsafe { std::mem::transmute(buf.ptr(hello)) };

    println!("before");
    hello_fn();
    println!("after");}

missing any parts? Based on dynasm-rs/README.md at master · CensoredUsername/dynasm-rs · GitHub , the only change is the syscall for the calling external function.

And as for the syscall, this is sysv64 api, so we are passing:

rax = 1 = write
rdi = 1 = stdout
rsi = msg = char *
rdx = length of the string

I don't see any issues with that code.

The crazy thing is that this works:

cat Makefile ; cat hello.asm 
hello: hello.o
        gcc -o hello hello.o -no-pie

hello.o: hello.asm
        nasm -f elf64 -g -F dwarf hello.asm -l hello.lst
; hello.asm
section .data
  msg db "hello, world", 0
section .bss
section .text
  global main

main:
  mov rax, 1
  mov rdi, 1
  mov rsi, msg
  mov rdx, 12
  syscall

so the problem is either (1) I am calling dynasm wrong or (2) rust's runtime somehow supress directly writing to stdout via syscall (tried running this with cargo test -- --nocapture -- no luck).

There are cool things to be learned in this forum. Dynasm seems really interesting.

When I try out your code in gdb, you can see that %rsi doesn't actually point to the string, it contains the string:

(gdb) p hello_fn
$1 = (*mut fn ()) 0x7ffff7ff300f
(gdb) x/20i $1
   0x7ffff7ff300f:	mov    $0x1,%rax
   0x7ffff7ff3016:	mov    $0x1,%rdi
   0x7ffff7ff301d:	mov    -0x24(%rip),%rsi        # 0x7ffff7ff3000
   0x7ffff7ff3024:	mov    $0xf,%rdx
   0x7ffff7ff302b:	syscall 
   0x7ffff7ff302d:	ret    
(gdb) b *0x7ffff7ff302b
Breakpoint 2 at 0x7ffff7ff302b
(gdb) info reg
rax            0x1                 1
rbx            0x7fffff7ff000      140737479962624
rcx            0x7ffff7ff300f      140737354084367
rdx            0xf                 15
rsi            0x77202c6f6c6c6568  8583909746840200552
rdi            0x1                 1

where

77202c6f6c6c6568
w   , o l l e h

strace confirms this as well:

write(1, "before\n", 7)                 = 7
write(1, 0x77202c6f6c6c6568, 15)        = -1 EFAULT (Bad address)
write(1, "after\n", 6)                  = 6

Try:

dynasm!(ops
        ; .arch x64
        ; mov rax, 1
        ; mov rdi, 1
        ; lea rsi, [-> hello]
        ; mov rdx, hello_world.len() as i32
        ; syscall
        ; ret
    );

Impressive detective work. Syscall now works. Thanks!

When I try to replace ; lea rsi, [-> hello] with ; mov rsi, -> hello, I get the following error:

error: 'mov': argument type/size mismatch, expected one of the following forms:
>>> mov reg/mem64, reg64
>>> mov reg/mem16, reg16
>>> mov reg/mem32, reg32
>>> mov reg/mem8, reg8
>>> mov reg64, reg/mem64
>>> mov reg16, reg/mem16
>>> mov reg32, reg/mem32
>>> mov reg8, reg/mem8
>>> mov reg64, segreg
>>> mov reg16, segreg
>>> mov reg32, segreg
>>> mov mem16, segreg
>>> mov segreg, mem16
>>> mov segreg, reg16
>>> mov reg8, imm8
>>> mov reg16, imm16
>>> mov reg32, imm32
>>> mov reg/mem64, imm32
>>> mov reg/mem16, imm16
>>> mov reg/mem32, imm32
>>> mov reg/mem8, imm8
>>> mov reg64, imm64
>>> mov creg, reg32
>>> mov creg, reg64
>>> mov reg32, creg
>>> mov reg64, creg
>>> mov cr8, reg32
>>> mov cr8, reg64
>>> mov reg32, cr8
>>> mov reg64, cr8
>>> mov dreg, reg32
>>> mov dreg, reg64
>>> mov reg32, dreg
>>> mov reg64, dreg

This surprises me because it contains the line >>> mov reg64, imm64 -- and I would assume that:

  • rsi: reg64
  • -> hello : imm64

It surprised me, too. I suppose we'd actually have to read the documentation that comes with it to learn what [ -> hello ] actually means. :wink:

current understanding:

[ ... ] is x86 notation for "de-ref"
-> foo is a "global reference"

lea is a special instruction which says: take the address instead of the value of the following expression; this is useful because lea supports certain ptr address exprs that mov does not

1 Like

I don't think Dynasm supports this.

See mov r64, imm64 seems to be unsupported · Issue #40 · CensoredUsername/dynasm-rs · GitHub and dynasm-rs/gen_opmap.rs at e10fb11e47e028422eb54fd55cea30af8fa7e0ad · CensoredUsername/dynasm-rs · GitHub - the opcode would be 0x48 0xBE, and there would be a need for a syntax to express: "treat this label as a 64-bit immediate" which is lacking. (In GNU asm, you'd use movabs $label for that)

If I am reading mov r64, imm64 seems to be unsupported · Issue #40 · CensoredUsername/dynasm-rs · GitHub correctly (and I'm 50-50 on whether I'm reading this correctly), it looks like dynasm does support it. The problem I am running into is dynasm parses [->hello] but refuses to parse ->hello, -- i.e. I can't figure out how to say ->hello without the [].

In particular, this is a parsing error on the ->hw

    let hello_world = "hello, world!\n\0";
    dynasm!(ops
        ; .arch x64
        ; ->hw:
        ; .bytes hello_world.as_bytes()
    );
    let hello = ops.offset();
    dynasm!(ops
        ; .arch x64
        ; mov rax, 1
        ; mov rdi, 1
        ; mov rsi, QWORD (->hw)
        ; mov rdx, hello_world.len() as i32
        ; syscall
        ; ret
    );

Note, the following 'works':

fn main() {
    let mut ops = dynasmrt::x64::Assembler::new().unwrap();
    let hello_world = "hello, world!\n\0";
    dynasm!(ops
        ; .arch x64
        ; ->hw:
        ; .bytes hello_world.as_bytes()
    );
    let hello = ops.offset();
    dynasm!(ops
        ; .arch x64
        ; mov rax, 1
        ; mov rdi, 1
        ; mov rsi, QWORD ( hello_world.as_bytes().as_ptr() as *const u8 as i64 )
        ; mov rdx, hello_world.len() as i32
        ; syscall
        ; ret
    );
    let buf = ops.finalize().unwrap();
    let hello_fn: extern "sysv64" fn() = unsafe { std::mem::transmute(buf.ptr(hello)) };

    println!("before");
    hello_fn();
    println!("after");}

this shows that the 'mov reg64, imm64' works (as we can give it the addr in Rust's memory); this seems to suggest the problem is "how to get the 'value' of ->hw"

You're right that this gives the correct (movabs) opcode, but the resulting code may be UB in that you're hardwiring the address of hello_world which will then go out of scope.

So you'd need a "take the address of a label as an immediate" syntax, which the developer rejected in 2016 as having too limited use.

Note that you'll want the lea instruction likely anyway since it uses %rip-relative addressing:

   0x7ffff7ff301d:	lea    -0x24(%rip),%rsi        # 0x7ffff7ff3000

I agree with you that it looks bad, which is why I put 'works' in quotes. I am not sure whether it is UB or not -- because (1) the function is executed before the function where the string is defined ends and (2) aren't string literals &'static str by default ?

Thanks for digging this up. This puts the issue to rest. Thanks for all your help with pushing things forward in this threat at all the places I got stuck.

I wasn't sure whether &'static str means that you can just take a pointer (given that the str slice isn't pinned but merely has a static lifetime, which means that references stay valid).

I tried it in Godbolt and when the rust is all scraped off, "static".as_bytes().as_ptr() turns into a const char * pointing straight into the ro section where the literal is located.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.