Side note: this is kinda short because I have to go do some errands in a bit. I'll write a longer tutorial about something sometime.
Interrupts vs. SYSENTER/SYSLEAVE
Linux used to use interrupt 80h to make its system calls; Intel introduced the SYSENTER and SYSLEAVE instructions to make things much faster. As SYSENTER and SYSLEAVE are somewhat confusing to the novice programmer (it requires writing to model-specific registers and a slew of other fun things), I'm going to stick with interrupts for now. As of this writing, the interrupts still work on my system.
Making a System Call
How do we make a system call in Linux? Well, we do it by passing certain values in registers. This convention always holds no matter what kind of call you're making. It goes something like this:
EAX - ID number of the system call you're making EBX - argument 1 ECX - argument 2 EDX - argument 3 ESI - argument 4 EDI - argument 5 EBP - argument 6If you only have three arguments, don't worry about the other registers; you don't have to zero them out or anything. You should not rely on the system saving your registers, so if you have anything you can't afford to lose--EBP being one of them--save it yourself. The only register you're guaranteed to get back is ESP. The rest of the arguments, if any, must be pushed on the stack following C convention (i.e. in reverse order).
So what about these ID numbers? There's a rather extensive list of them here. Notice that there's a heading called "Section 2 - system calls" on the main page. This means that when we get our search results, we're going to be looking for write(2) because that's the entry for write() in--guess what--section 2, our list of system calls.
So we do our search and find this:
This means that we have our file descriptor as our first argument, a pointer to whatever it is that we're going to write as the second, and the number of bytes we want to write as our third. Translating that to our system call convention, we have:
ssize_t write(int fd, const void *buf, size_t count);
mov eax, 4 mov ebx, (STDOUT's file descriptor) mov ecx, (pointer to our Hello World string) mov edx, (number of bytes to write) int 80hNot too much of a problem. The file descriptor for STDOUT is always 1. (STDIN, STDOUT, STDERR -> 0,1,2 respectively.) Getting a pointer in NASM is simple, as you just put the label name and NASM does the rest for you. As for the number of bytes, we're just going to cheat for now and count how many characters there are in the string "Hello, World!" It's 13 by my count, not including the terminating null. We don't need it. So our final system call is going to look like this:
mov eax, 4 mov ebx, 1 mov ecx, sz_hello_world mov edx, 13 int 80h . . . ; somewhere in your .data section sz_hello_world: db "Hello, World!"Neat, huh?
I trust you're able to put a program together around this, but in case you can't, here's the full code:
global main section .code main: push ebp mov ebp, esp mov eax, 4 mov ebx, 1 mov ecx, sz_hello_world mov edx, 15 ; see note below int 80h pop ebp ret section .data ; note that we don't care about the terminating null, ; but I'm sticking in a carriage return / linefeed to ; get a new line on the console. sz_hello_world: db "Hello, World!",0dh,0ahCompile it with NASM, link with GCC, run it in your console...and behold the glory of Linux and assembly language.
A few side notes:
- You're going to want to check the return value (always EAX) to see if some error occurred. Negative values are always bad.
- To really make your print function useful, link with the standard library and call strlen (or roll your own) and stick the return value into EDX.
This also means that your strings now require the terminating null.
Questions? Comments? Insults? Feel free to post here so I can make further tutorials better.
Next In This Series
Edited by dargueta, 18 November 2010 - 07:24 PM.
Wrong file descriptors mentioned and forgot link.