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.
![](https://matspetterss.wordpress.com/wp-content/uploads/2023/01/helloint.png?w=632)
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:
Parameter | Register (or stack) |
---|---|
1 | rdi |
2 | rsi |
3 | rdx |
4 | rcx |
5 | r8 |
6 | r9 |
7 | [rsp + 0x10] |
8 | [rbp + 0x18] |
9 | [rbp + 0x20] |
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
![](https://matspetterss.wordpress.com/wp-content/uploads/2023/01/linux64-helloc.png?w=856)
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.
![](https://matspetterss.wordpress.com/wp-content/uploads/2023/01/win64-nmake.png?w=1024)
The output
Running the 64-bit windows program gives the same output as for Linux 🙂
![](https://matspetterss.wordpress.com/wp-content/uploads/2023/01/win64-helloc.png?w=1024)
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:
Parameter | Register (or stack) |
---|---|
1 | rcx |
2 | rdx |
3 | r8 |
4 | r9 |
5 | [rsp + 30h] |
6 | [rsp + 38h] |
7 | [rsp + 40h] |
8 | [rsp + 48h] |
9 | [rsp + 50h] |
Assembling the new helloint.asm using nasm:
nasm -f win64 helloint.asm -o helloint.obj
And linking with the already compiled helloc.obj:
![](https://matspetterss.wordpress.com/wp-content/uploads/2023/01/win64-nasm.png?w=1024)
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 🙂
Leave a comment