Почему параметры располагаются в стеке при вызове функции? - PullRequest
0 голосов
/ 01 апреля 2020

Я следую руководству по разработке ОС. Там мне нужно реализовать функцию, которая получает адрес (длиной 2 байта) порта ввода / вывода, данные (длиной 1 байт) для отправки на этот порт и отправляет данные на указанный порт.

Это должно быть реализовано в сборке (NASM) и использовано в коде C через определенный заголовок функции. Вот решения из учебника:

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 */

Мой вопрос о этой части:

; stack: [esp + 8] the data byte
;        [esp + 4] the I/O port
;        [esp    ] return address

Я строю для 32-битной среды, поэтому 4-байтовая разница между адресом return address и адресом I/O port имеет смысл - это из-за return address длиной 4 байта. Но почему разница между адресами I/O port и data byte также равна 4?

Я подумал, что когда я вызываю функцию в C, она напрямую помещает аргументы в стек, затем отправляет адрес возврата и переходит функционировать (имеется в виду, что в моем понимании data byte должно быть [esp + 6] (4 байта return address + 2 байта I/O port) вместо [esp + 8]), но, похоже, он также выравнивает параметры по 4 байта границы, но я не уверен в этом.

Это происходит из-за флага -m32? Я читал об этом флаге в документации по GNU, и в нем говорится:

-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.

Так что, похоже, это только меняет размеры int / long / pointers. Так почему сборочная сторона «уверена», что параметры будут на границе 4 байта? Это просто соглашение? И если да, то зачем это нужно?

Вот все флаги, которые я использую для сборки:

CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \
         -nostartfiles -nodefaultlibs -Wall -Wextra -Werror

LDFLAGS = -T link.ld -melf_i386
ASFLAGS = -f elf32

1 Ответ

3 голосов
/ 01 апреля 2020

Так почему же сторона сборки «уверена», что параметры будут на границе 4 байта? Это просто соглашение?

Да, это соглашение. То, что вы видите, это соглашение о вызовах c32 для IA32, которое является соглашением о вызовах по умолчанию, используемым большинством компиляторов в IA32 (32-разрядная версия x86).

Из документации G CC :

cdecl

В целях x86-32 атрибут cdecl заставляет компилятор предположить, что вызывающая функция выскакивает из стекового пространства, используемого для передачи аргументов. Это полезно для отмены эффектов ключа -mrtd.

Это соглашение о вызовах предполагает, что параметры будут помещены в стек вызывающей стороной и popped потом. Поскольку инструкции push и pop работают с размером регистра, значение push / pop в IA32 всегда приводит к выталкиванию / извлечению значения 4 байта из стека. Конечно, меньшие значения могут быть переданы с помощью sub esp, x + mov, что приведет к меньшему смещению стека, но это не то, что предписывает это соглашение.

И, конечно, передача аргументов может быть сделано с другими инструкциями тоже; Соглашение о вызовах не имеет значения , как вы помещаете данные в память над указателем стека до call, они просто должны быть там, где вызываемый объект ожидает их найти. В зависимости от оптимизации или настроек -mtune= для старых процессоров, -maccumulate-outgoing-args может быть включено , в результате чего G CC не будет использовать push.

И если да зачем это нужно?

На самом деле это не нужно, это просто стандартное соглашение о вызовах для IA32. Вы можете указать другое соглашение о вызовах, если хотите: просто используйте __attribute__((xxx)) с одним из атрибутов, определенных в документации, которую я связал выше, и не забудьте обновить код сборки в соответствии с выбранным соглашением о вызовах.

Остерегайтесь тем не менее, если вы используете этот подход, ваш код будет зависеть от компилятора (например, только GNU-совместимые компиляторы поймут его, как G CC и clang), а другие компиляторы, которые работают по умолчанию с соглашением cdecl IA32, могут не распознавать атрибут и ошибка отсутствуют или даже не могут сгенерировать правильный код.

Например, __attribute__((regparm(3))) получит G CC для эффективной передачи первых 3 аргументов в регистрах вместо памяти даже в 32-битном коде. Ядро Linux использует gcc -mregparm=3 для 32-разрядных сборок, потому что вызовы из пространства пользователя должны go через системные вызовы, поэтому ничто не мешает ядру использовать соглашение о вызовах, отличное от пространства пользователя.

...