I'm following an OS development tutorial. There I need to implement a function that receives address (2 bytes long) of I/O port, data (1 byte long) to be sent into that port, and sends the given data to the given port.
This should be implemented on Assembly (NASM) and used in C code through defined function header. Here are solutions from tutorial:
io.s
global outb ; make the label outb visible outside this file
; outb - send a byte to an I/O port
; stack: [esp + 8] the data byte
; [esp + 4] the I/O port
; [esp ] return address
outb:
mov al, [esp + 8] ; move the data to be sent into the al register
mov dx, [esp + 4] ; move the address of the I/O port into the dx register
out dx, al ; send the data to the I/O port
ret ; return to the calling function
io.h
#ifndef INCLUDE_IO_H
#define INCLUDE_IO_H
/** outb:
* Sends the given data to the given I/O port. Defined in io.s
*
* @param port The I/O port to send the data to
* @param data The data to send to the I/O port
*/
void outb(unsigned short port, unsigned char data);
#endif /* INCLUDE_IO_H */
My question is about this part:
; stack: [esp + 8] the data byte
; [esp + 4] the I/O port
; [esp ] return address
I am building for 32 bit environment, so the 4 byte difference between the address of return address
and the address of I/O port
makes sense - it's because of return address
is 4 bytes long. But why is the difference between the addresses of I/O port
and data byte
also 4?
I thought that when I call a function in C it directly pushes arguments in stack, then pushes return address and jumps to function (meaning that in my understanding, data byte
should be [esp + 6]
(4 bytes of return address
+ 2 bytes of I/O port
) instead of [esp + 8]
), but it seems that it's also aligning parameters on 4 byte boundary, but I'm not sure about this.
Is this happening because of -m32
flag? I did read about this flag in GNU documentation and it states:
-m32
-m64
Generate code for a 32 bit or 64 bit environment. The 32 bit environment sets int, long and
pointer to 32 bits. The 64 bit environment sets int to 32 bits and long and pointer to 64
bits.
So it looks like this only changes the sizes of int / long / pointers. So why is assembly side 'sure' that parameters will be on 4 byte boundary? Is this just a convention? And if yes, why is it needed?
Here are all the flags I'm using for building:
CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \
-nostartfiles -nodefaultlibs -Wall -Wextra -Werror
LDFLAGS = -T link.ld -melf_i386
ASFLAGS = -f elf32
-mpreferred-stack-boundary=4
(2^4 = 16) is the usual default. Oh, when you said "aligning" you meant each parameter separately. Yes, every arg takes a whole "stack slot". at minimum – Doorjamb