1

Can someone help me write a program that calculates the average of 3 numbers in assembly (NASM 64 bit)?

What I tried:

section .data
    num1 db 3
    num2 db 4
    num3 db 5
    divisor db 3
    digit db 0, 10

section .text
    global _start
    _start:
        mov rax, num1
        mov rax, num2
        div rax, num3
        mov rbx, divisor
        div rbx
        mov rax, 60
        mov rdi, 0
        syscall
    _printRAX:
        add rax, 48
        mov [digit], al
        mov rax, 1
        mov rdi, 1
        mov rsi, digit
        mov rdx, 2
        syscall
        ret
hidefromkgb
  • 5,834
  • 1
  • 13
  • 44
Incognite
  • 41
  • 1
  • 3
  • 2
    Q: What is the OS? Q: What "syscall" are you making to that OS? Q: What exactly is your question/what problem you're encountering? – paulsm4 Oct 03 '18 at 20:22
  • 1
    I'm confused, what do you think finding the average looks like not in assembly? Seems like you move a bunch of stuff into rax. – Dave Newton Oct 03 '18 at 20:31
  • `mov rax, num1` sets `rax` to the address of `num1`, in NASM syntax. `mov rax, [num1]` would be a 64-bit load that sets `rax = 0x????0a0003050403` (x86 is little-endian and `db` is only 1 byte wide). Besides all that, you're leaving RDX unset. It needs to be zeroed for `div`. In a statically-linked binary, Linux will zero it before `_start`, but anywhere else this code can crash with SIGFPE from the #DE divide exception. – Peter Cordes Oct 03 '18 at 20:40
  • Anyway, you want either `movzx eax, byte [num1]` and so on into different registers to set up for `add`, or you want `add al, [num2]` and so on for the later numbers. – Peter Cordes Oct 03 '18 at 20:49
  • @paulsm4 the OS is Linux 64bit (mint). – Incognite Oct 03 '18 at 21:21

1 Answers1

1

OK, here are two examples that might help:

EXAMPLE 1:

;
; Standalone NASM "Hello world"
;
; BUILD:
; nasm -f elf64 hello.asm
; ld -s -o hello hello.o
;
; EXAMPLE OUTPUT:
; Hello, world!
;
section .text                   ;code section (shareable between processes)
    global  _start              ;loader entry point

_start:
    mov     edx,len             ;arg3: msg len
    mov     ecx,msg             ;arg2: *msg
    mov     ebx,1               ;arg1: 1 (stdout)
    mov     eax,4               ;syscall@4 (sys_write)
    int     0x80

    mov     ebx,0               ;arg1: exit code (0)
    mov     eax,1               ;sycall@1 (sys_exit)
    int     0x80

section .data                   ;data section (per process)
msg db      "Hello, world!",0xa ;our dear string
len equ     $ - msg             ;length of our dear string

EXAMPLE 2:

;
; "Hello world" using standard C library
;
; BUILD:
; nasm -f elf64 avg3.asm
; gcc -m64 -o avg avg.o
;
; EXAMPLE OUTPUT:
; sum=12
; avg=4
;
extern  printf                  ;stdlib C function

section .text                   ;code section
    global  main                ;standard GCC entry point

main:
    push rbp                    ;set up stack frame: must be aligned

    ; Add 3+4+5
    mov     rax,3
    add     ax,4
    add     ax,5

    ; Save and print sum
    push    rax
    mov     rdi,fmt1            ;printf format string
    mov     rsi,rax             ;1st parameter
    mov     rdx,0               ;No 2nd parameter
    mov     rax,0               ;No xmm registers
    call    printf

    ; Compute and print average
    mov     dx,0                ;Clear dividend, high
    pop     rax                 ;dividend, low <= sum
    mov     cx,3                ;divisor
    div     cx                  ;ax= high, dx= low

    ; Print average
    mov     rdi,fmt2            ;printf format string
    mov     rsi,rax             ;1st parameter
    mov     rdx,0               ;No 2nd parameter
    mov     rax,0               ;No xmm registers
    call    printf

    ; Exit program
    pop rbp
    mov rax,0
    ret

section .data                   ;data section
fmt1:
    db      "sum=%d",0xa,0
fmt2:
    db      "avg=%d",0xa,0

NOTES:

  • Personally, I prefer "Gnu Assembler" (gas). It makes it easy to switch between different architectures, and between C and in-line assembler, without the "cognitive dissonance" of dealing with Intel syntax ;)

  • I'd strongly urge you to leverage the standard C library has much as possible. In practice, this means linking your executable using gcc instead of ld.

    Your "compute average" program is a great example why: it's much easier to let the printf formatter figure out the correct output, rather than converting your binary values to ASCII digits, and then figuring out how to format them into a string manually.

'Hope that helps!

paulsm4
  • 114,292
  • 17
  • 138
  • 190
  • 1
    In your first example, `int 0x80` only happens to work in 64-bit code because you were using `sys_write` on a static buffer, and you made a position-dependent (non-PIE) executable so that static code/data is in the low 32 bits of virtual address space. [What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?](https://stackoverflow.com/q/46087730). – Peter Cordes Oct 03 '18 at 23:19
  • Point well taken - thank you. I was just trying to create the "simplest possible example". I'd be happy to update the example if you suggest "something better". – paulsm4 Oct 03 '18 at 23:21
  • In your 2nd example, you violate the ABI by calling `printf` with the stack not aligned by 16 (2x `push` after function entry). It happens to work on current glibc (with AL=0), but it's allowed to segfault. Instead of using RBP for a frame pointer, use it to hold the sum. e.g. `mov ebp, 3` / `add ebp, 4+5` / `mov esi, ebp` / ... printf / `mov eax, ebp` / `xor edx,edx` / `div` / `mov esi, eax`. (Or make only one printf call with 3 args, so you don't need to save/restore the sum across it.) There's also no benefit to using 16-bit registers here. – Peter Cordes Oct 03 '18 at 23:23
  • To fix your first example: the OP already uses 64-bit `sys_write` and `sys_exit`, and my answer on [What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?](https://stackoverflow.com/q/46087730) has a 64-bit Hello World (including some extra stuff to make an example for that answer, but it can be stripped down.) – Peter Cordes Oct 03 '18 at 23:26
  • Q: What part of `push rax` doesn't leave the stack aligned by a multiple of 16? I'm not "arguing" - I'm just curious what would be a "valid push" in this example. – paulsm4 Oct 03 '18 at 23:29
  • BTW, when you say you like GAS for mental gear-switching to other architectures, are you talking about assembler directives like `.globl` and `.p2align`? Or are you talking about operand-order with some other architecture that puts destination last? Most put destination first, making Intel syntax much less of a gear-switch for me. Plus, Intel's ISA reference manuals (HTML extract: http://felixcloutier.com/x86/index.html) are by far the best thing to look at when learning any tricky details of what instruction does what, and it uses Intel syntax. – Peter Cordes Oct 03 '18 at 23:29
  • Alignment: Before a `call`, the stack must be 16-byte aligned. Thus, after a call (on entry to `main`), `rsp+8` is aligned. `push rbp` aligns the stack, `push rax` misaligns it again. You need an odd number of pushes before a `call`, or a `sub rsp, 16*n+ 8`. In this case, exactly 1 `push` is all you need. Probably your best bet (other than a single printf) is to divide first and save the quotient, reducing the amount of instructions. – Peter Cordes Oct 03 '18 at 23:30