Silly explorations in assembly part 1

I actually don’t know what triggered me, but I wanted to better understand the differences in how parameters are passed between functions in different operating systems based on the Intel architecture.

At the same time I wanted to keep the scope limited and reuse one C program to be used as driver, and one assembler program that unpacks the parameters and does some simple calculations.

The helloint() function takes on argument and returns 0 if the value is even and 1 if it is odd. But in order to understand how to do it with 9 parameters I created the helloints() function that recieves nine integers and returns the sum of them.

The helloc.c C program

No big surprise, but the C program can be reused and just be recompiled for the different platforms. The two functions in the assembly file are declared as extern and then linked together at a later stage. There is a hello world message printed, based on a variable defined as a compiler switch.

#include <stdio.h>

#ifdef hellolin64
#define hellostr "Hello, world! Linux 64-bit :-)"
#endif

#ifdef hellowin64
#define hellostr "Hello, world! Windows 64-bit :-)"
#endif

#ifndef hellostr
#define hellostr "Hello, world! Default..."
#endif

extern int helloint(int );
extern int helloints(int , int , int , int , int , int , int , int , int );

int main(int argc, char *argv[]) {
char *hello = hellostr;
int a = 0;
int rc = 0;

	printf("%s\n",hello);

	a = 0;
	rc = helloint(a);
	printf("Return value: %d, Integer a = %d is %s\n", rc, a, rc ? "odd" : "even");
	a = 1;
	rc = helloint(a);
	printf("Return value: %d, Integer a = %d is %s\n", rc, a, rc ? "odd" : "even");
	a = 2;
	rc = helloint(a);
	printf("Return value: %d, Integer a = %d is %s\n", rc, a, rc ? "odd" : "even");
	a = 3;
	rc = helloint(a);
	printf("Return value: %d, Integer a = %d is %s\n", rc, a, rc ? "odd" : "even");
	a = 8;
	rc = helloint(a);
	printf("Return value: %d, Integer a = %d is %s\n", rc, a, rc ? "odd" : "even");

	rc = helloints(1, 2, 3, 4, 5, 6, 7, 8, 9);
	printf("rc=%d (%d %d %d %d %d %d %d %d %d)\n", rc, 1, 2, 3, 4, 5, 6, 7, 8, 9);
	rc = helloints(1, 1, 1, 1, 1, 5, 0, 0, 0);
	printf("rc=%d (%d %d %d %d %d %d %d %d %d)\n", rc, 1, 1, 1, 1, 1, 5, 0, 0, 0);
	rc = helloints(9, 9, 9, 9, 10, 10, 10, 10, 10);
	printf("rc=%d (%d %d %d %d %d %d %d %d %d)\n", rc, 9, 9, 9, 9, 10, 10, 10, 10, 10);
}

I started with 64-bit Linux. Then 64-bit Windows. To go back in history I got curious about 32-bit OS/2, and also 16-bit OS/2. And last but not least 16-bit DOS. I’ve been programming since the early 80s, so for me this is also a kind of time travel experience. But before going back in history, let’s get started with the 64-bit world!

64-bit Linux

For Linux I use GNU Compiler Collection (GCC) and The Netwide Assembler (NASM).

Installing GCC and NASM in Ubuntu is easy:

sudo apt install build-essential nasm

The programs are really simple and do not, so there is not much to say about that. The important thing is to understand how to fetch the parameters in the assembly module. Then I am just adding and doing simple logic. So I will not go into details here.

The Application Binary Interface (ABI) is a good source for understanding calling conventions and how to for example pass parameters. Since I’m only exploring the calling convention, the short story is:

ParameterRegister (or stack)
1rdi
2rsi
3rdx
4rcx
5r8
6r9
7[rsp + 0x10]
8[rbp + 0x18]
9[rbp + 0x20]
Parameters 1 to 9

Here is the complete assembly file, with the two different functions – helloint() and helloints(…).

The helloint.asm file
global helloint:function
global helloints:function

section .text

helloint:
      push	rbp
      mov   rbp, rsp

      mov   rax, rdi   ; 1st parameter
      and   rax, 1      ; Test if odd or even
      jz    even
      mov   rax, 1
      jmp   finish
even:
     mov  rax, 0      ; return code in rax
finish:
      mov	rsp,rbp
      pop	rbp
      ret

helloints:
      push	rbp
      mov   rbp, rsp

      mov   rax, rdi    ; 1st parameter
      add   rax, rsi     ; 2
      add   rax, rdx    ; 3
      add   rax, rcx    ; 4
      add   rax, r8     ; 5
      add   rax, r9     ; 6
      add   rax, [rbp + 0x10]   ; 7th parameter, and the rest via stack
      add   rax, [rbp + 0x18]   ; 8
      add   rax, [rbp + 0x20]   ; 9

      pop	rbp
      ret
Putting it together

I do the compiling, assembling and linking using the following makefile:

all: helloc 

helloc: helloc.o helloint.o
	gcc helloc.o helloint.o -o helloc -z noexecstack

helloc.o : helloc.c
	gcc -c helloc.c

helloint.o: helloint.asm
	nasm  -f elf64 -l helloint.lst helloint.asm

clean:
	rm helloc.o helloint.o helloint.lst 

The -f elf64 switch for nasm specifies the executable file format (64-bit). ELF means Executable and Linkable Format.

I can’t really explain the -z noexecstack switch in the link step, but it removes a warning message 🙂

The output

Windows 11 – 64-bit

For developing in 64-bit Windows, I first decided to use the Microsoft tooling.

  • Visual Studio
  • Visual C++
  • Microsoft macro assembler (ml64)
The helloint.asm file
PUBLIC helloint
PUBLIC helloints

_DATA SEGMENT
_DATA ENDS

_TEXT SEGMENT

helloint PROC
      push	rbp
      mov   rbp, rsp

      mov   rax, rcx
      and   rax, 1
      jz    even64
      mov   rax, 1
      jmp   finish64
even64:
     mov  rax, 0
finish64:
      mov	rsp,rbp
      pop	rbp
      ret

helloint ENDP


helloints   PROC
      push	rbp
      mov   rbp, rsp

      mov   rax, rcx   ; 1st parameter
      add   rax, rdx   ; 2 ...
      add   rax, r8
      add   rax, r9
      add   rax, [rsp + 30h]
      add   rax, [rsp + 38h]
      add   rax, [rsp + 40h]
      add   rax, [rsp + 48h]
      add   rax, [rsp + 50h]

      mov	rsp,rbp
      pop	rbp

      ret
helloints ENDP

_TEXT ENDS

END
Putting it together – The makefile
all: helloc.exe

helloc.exe: helloc.obj helloint.obj
	link helloc.obj helloint.obj

helloc.obj : helloc.c
	cl -c helloc.c -D hellowin64

helloint.obj: helloint.asm
	ml64 /c /Flhelloint.lst helloint.asm

clean:
	del helloc.obj helloc.exe helloint.obj helloint.lst

Running nmake to produce the executable.

The output

Running the 64-bit windows program gives the same output as for Linux 🙂

What about other tools for Windows?

I decided to also try the NASM assembler for Windows. The Microsoft masm syntax differs from nasm. But I already had the source from the Linux example, and the only thing that actually differs is how the parameters are passed. The rest is the same.

global helloint
global helloints

section .text

helloint:
      push	rbp
      mov   rbp, rsp

      mov   rax, rcx
      and   rax, 1
      jz    even64
      mov   rax, 1
      jmp   finish64
even64:
     mov  rax, 0
finish64:
      mov	rsp,rbp
      pop	rbp

      ret

helloints:
      push	rbp
      mov   rbp, rsp

      mov   rax, rcx
      add   rax, rdx
      add   rax, r8
      add   rax, r9
      add   rax, [rsp + 30h]
      add   rax, [rsp + 38h]
      add   rax, [rsp + 40h]
      add   rax, [rsp + 48h]
      add   rax, [rsp + 50h]

      mov	rsp,rbp
      pop	rbp

      ret

This is how you retrieve the parameters in 64-bit Windows:

ParameterRegister (or stack)
1rcx
2rdx
3r8
4r9
5[rsp + 30h]
6[rsp + 38h]
7[rsp + 40h]
8[rsp + 48h]
9[rsp + 50h]
Parameters 1 to 9

Assembling the new helloint.asm using nasm:

nasm -f win64 helloint.asm -o helloint.obj

And linking with the already compiled helloc.obj:

To summarize

The same C file could be used for both Linux and Windows, which was expected. The assembler file needed to be adjusted for masm instead of nasm, and also the calling convention differed, but all in all it worked well to reuse the code.

I will continue exploring OS/2 and DOS in the next post. So stay tuned for that 🙂


Posted

in

,

by

Comments

Leave a comment

Design a site like this with WordPress.com
Get started