Chapter 8: Going from DOS to Linux or Windows

In the unlikely event that you have read the first 7 chapters of this book, I am going to assume you are a pretty hard core computer user. What I can say for sure is that you are the type of person who reads books or blog posts about technical details. DOS is an operating system that tends to only be used by nerds who love reading text and efficient operations at the command line.

Sadly to say, our kind is dying out. At the time of writing this I am 38 years old and there are few people who remember the old way computers were used. DOS is mostly seen as a dead platform and it is not usually used except by programmers and hard core gamers who still run their favorite games in a DOS emulator, although I cannot fail to mention that FreeDOS is available as a real DOS system.

But most people know nothing about DOS because the popular operating systems available today are Windows, MacOS and Linux.

If you have enjoyed programming in Assembly, I do have some helpful tips on how you can apply most of the same information to start Assembly in Linux.

As far as Windows or MacOS go, I cannot help you much with that because I don’t use proprietary operating systems if I have a choice. These operating systems don’t allow you to simply load registers and call interrupts to print things on the screen.

Linux, however, works very much like DOS does. If you know how to load the registers correctly and use a system call, you can print strings of text just like in DOS except MUCH faster because you will be running natively instead of in an emulator as in the DOS examples from the rest of this book.

I cannot cover the details of installing a Linux operating system because there are many choices. However I recommend Debian because it has been my main distro for years. Therefore, the following two programs that I will show you in this chapter have both been tested to work on my 64 bit Intel PC running Debian 12 (bookworm).

Remember, although DOS was a 16 bit system, modern Linux processors and distros usually support 32 or 64 bit code. Therefore, I will be showing you a small program using the FASM assembler that prints text using a Linux version of the putstring function. It behaves the same as the DOS version behaves in chapter 2.

main.asm (32 bit)

 1 format ELF executable
 2 entry main
 3 
 4 main:
 5 
 6 mov eax,main_string
 7 call putstring
 8 
 9 mov eax, 1  ; invoke SYS_EXIT (kernel opcode 1)
10 mov ebx, 0  ; return 0 status on exit - 'No Errors'
11 int 80h
12 
13 ;A string to test if output works
14 main_string db 'This program runs in Linux!',0Ah,0
15 
16 putstring:
17 
18 push eax
19 push ebx
20 push ecx
21 push edx
22 
23 mov ebx,eax ; copy eax to ebx. ebx will be used as index to the string
24 
25 putstring_strlen_start: ; this loop finds the length of the string as part of the putstring function
26 
27 cmp [ebx],byte 0 ; compare byte at address ebx with 0
28 jz putstring_strlen_end ; if comparison was zero, jump to loop end because we have found the length
29 inc ebx
30 jmp putstring_strlen_start
31 
32 putstring_strlen_end:
33 sub ebx,eax ;By subtracting the start of the string with the current address, we have the length of the string.
34 
35 ; Write string using Linux Write system call. Reference for 32 bit x86 syscalls is below.
36 ; https://www.chromium.org/chromium-os/developer-library/reference/linux-constants/syscalls/#x86-32-bit
37 
38 mov edx,ebx      ;number of bytes to write
39 mov ecx,eax      ;pointer/address of string to write
40 mov ebx,1        ;write to the STDOUT file
41 mov eax,4        ;invoke SYS_WRITE (kernel opcode 4 on 32 bit systems)
42 int 80h          ;system call to write the message
43 
44 pop edx
45 pop ecx
46 pop ebx
47 pop eax
48 
49 ret ; this is the end of the putstring function return to calling location
50 
51 ; This Assembly source file has been formatted for the FASM assembler.
52 ; The following 3 commands assemble, give executable permissions, and run the program
53 ;
54 ;   fasm main.asm
55 ;   chmod +x main
56 ;   ./main

The program above uses only two system calls. One is the call to exit the program. The other is the write call which is the same as the DOS function 0x40 of interrupt 0x21; However, the usage of the registers is not in the same order. However, these registers: eax,ebx,ecx,edx are the same registers except that they are extended to 32 bits. That is why they have an e in their name.

But if you take the time to study it, you will see that it does the exact same process of finding the length of the string by the terminating zero and then loading the registers in such a way that the operating system knows what function we care calling, which handle we are writing to, how many bytes to write, and where the data is in memory which will be written.

Next I will show you the 64-bit equivalent that works the same way but uses different numbers for the system calls.

main.asm 64 bit

 1 format ELF64 executable
 2 entry main
 3 
 4 main: ; the main function of our assembly function, just as if I were writing C.
 5 
 6 mov rax,main_string ; move the address of main_string into rax register
 7 call putstring
 8 
 9 mov rax, 60 ; invoke SYS_EXIT (kernel opcode 60 on 64 bit systems)
10 mov rdi,0   ; return 0 status on exit - 'No Errors'
11 syscall
12 
13 ;A string to test if output works
14 main_string db 'This program runs in Linux!',0Ah,0
15 
16 putstring:
17 
18 push rax
19 push rbx
20 push rcx
21 push rdx
22 
23 mov rbx,rax ; copy rax to rbx as well. Now both registers have the address of the main_string
24 
25 putstring_strlen_start: ; this loop finds the length of the string as part of the putstring function
26 
27 cmp [rbx],byte 0 ; compare byte at address rdx with 0
28 jz putstring_strlen_end ; if comparison was zero, jump to loop end because we have found the length
29 inc rbx
30 jmp putstring_strlen_start
31 
32 putstring_strlen_end:
33 sub rbx,rax ;rbx will now have correct number of bytes
34 
35 ;write string using Linux Write system call
36 ;https://www.chromium.org/chromium-os/developer-library/reference/linux-constants/syscalls/#x86_64-64-bit
37 
38 mov rdx,rbx      ;number of bytes to write
39 mov rsi,rax      ;pointer/address of string to write
40 mov rdi,1        ;write to the STDOUT file
41 mov rax,1        ;invoke SYS_WRITE (kernel opcode 1 on 64 bit systems)
42 syscall          ;system call to write the message
43 
44 pop rdx
45 pop rcx
46 pop rbx
47 pop rax
48 
49 ret ; this is the end of the putstring function return to calling location
50 
51 
52 ; This Assembly source file has been formatted for the FASM assembler.
53 ; The following 3 commands assemble, give executable permissions, and run the program
54 ;
55 ;   fasm main.asm
56 ;   chmod +x main
57 ;   ./main

You may notice that the 64-bit program also uses the syscall instruction rather than interrupt 0x80. On my machine both programs behave identically because both calling conventions are valid. There are executables that run in 32 bit mode and others that run in 64 bit mode. They are not usually compatible and the FASM assembler has to be told which format is being assembled.

FASM has been my preferred assembler for a long time because unlike NASM, it has everything it needs to create executables without depending on a linker.

“What is a linker?” You might be asking. You see, the developers of Linux never really expected for people to be writing applications entirely in assembly. Usually they are written in C and then GCC compiles it to assembly that only the Gnu assembler (informally called Gas) can assemble and then link with the standard library. There is a linker program called “ld” that GCC automatically uses.

However, through some research and experimentation, I have converted the previous 64 bit FASM program into the Gas syntax. As you read it, remember that the AT&T phone company made this weird alternative syntax. The source and destination have been flipped so you will see the register receiving data on the right side instead of the left.

main.s (GNU Assembler 64 bit)

 1 # Using Linux System calls for 64-bit
 2 # Tested with GNU Assembler on Debian 12 (bookworm)
 3 # It uses Chastity's putstring function for output
 4 
 5 .global _start
 6 
 7 .text
 8 
 9 _start:
10 
11 mov $main_string,%rax # move address of string into rax register
12 call   putstring      # call the putstring function Chastity wrote
13 mov    $0x3c,%eax     # system call 60 is exit
14 mov    $0x0,%edi      # we want to return code 0
15 syscall               # end program with system call
16 
17 main_string:
18 .string "This program runs in Linux!\n"
19 
20 putstring:            # the start of the putstring function
21 push   %rax
22 push   %rbx
23 push   %rcx
24 push   %rdx
25 mov    %rax,%rbx
26 
27 putstring_strlen_start:
28 cmpb   $0x0,(%rbx)
29 je     putstring_strlen_end
30 inc    %rbx
31 jmp    putstring_strlen_start
32 
33 putstring_strlen_end:
34 sub    %rax,%rbx # subtract rax from rbx for number of bytes to write
35 mov    %rbx,%rdx # copy number of bytes from rbx to rdx
36 mov    %rax,%rsi # address of string to output
37 mov    $0x1,%edi # file handler 1 is stdout
38 mov    $0x1,%rax # system call 1 is write
39 syscall
40 pop    %rdx
41 pop    %rcx
42 pop    %rbx
43 pop    %rax
44 ret
45 
46 # This Assembly source file has been formatted for the GNU assembler.
47 # The following makefile rule has commands to assemble, link, and run the program
48 #
49 #main-gas:
50 #   gcc -nostdlib -nostartfiles -nodefaultlibs -static main.s -o main
51 #   strip main
52 #   ./main

Although I find the GNU Assembler syntax hard to read, the fact that this assembler exists as part of the GNU Compiler Collection means that it is usually available even on systems that don’t have FASM or NASM available.

It is possible to use NASM also but it can’t create executables and requires linking with “ld” anyway. It is better to just write directly for the GNU Assembler or stick with FASM if you prefer intel syntax.

However, the beauty is that the machine code bytes from both types of assembly are identical! In fact that is how I got the GAS version. I had to assemble the other version and then disassemble it with objdump to get the equivalent syntax.

The programs you saw in this chapter only work on Linux, but Linux is Free both in terms of Software Freedom and Free in price too because anyone with an internet connection can download the ISO of a new operating system and install it on their computer as long as they take the time to read directions from the makers of that distribution. In fact Debian, Arch, Gentoo, and FreeBSD (not Linux but very similar) all have great instruction manuals. If you have managed to read this book, then you will have no problem following their stuff.