#assembly #x86 #system-calls #api-design #calling-convention
#сборка #x86 #системные вызовы #api-дизайн #соглашение о вызове
Вопрос:
Я пытаюсь изучить сборку, и я могу заставить работать несколько примеров, но это вызывает недоумение.
Как ядро узнает, что нужно захватить то, что находится в ecx
регистре, в качестве указателя на память пользовательского пространства для отображения stdout
mov edx,9 ;message length
mov ecx, name ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
int 0x80 ;call kernel
Если edx — это универсальный регистр данных, а eax — универсальный ввод-вывод, почему вызов ядра должен ожидать данные / вывод в регистре ecx?
Комментарии:
1. В этих обозначениях нет ничего «универсального». Это просто регистры, места для хранения данных. То, как они используются, зависит от разработчика программного обеспечения. В этом случае люди, которые написали ядро ABI, решили, что второй аргумент системного вызова должен находиться в регистре ecx, и код ядра, который реализует системный вызов, написан для получения его из этого регистра.
2. Это потому, что ядро ищет в этом регистре эту базу данных. Линус мог бы также выбрать другой регистр, но он этого не сделал.
Ответ №1:
Расположение аргументов является частью ABI. За https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux#Making_a_syscall:
Параметры передаются путем установки регистров общего назначения следующим образом:
Syscall # | Param 1 | Param 2 | Param 3 | Param 4 | Param 5 | Param 6 eax | ebx | ecx | edx | esi | edi | ebp Return value eax
Ответ №2:
… почему вызов ядра должен ожидать данные / выходные данные в регистре ecx?
Прерывание — это особая форма подпрограммы, которая работает аналогично подпрограмме, которую вы вызываете с помощью call
инструкции.
Когда вводится прерывание, первое, что делается, — это для push
всех регистров в стеке. Это означает, что все регистры будут сохранены в оперативной памяти (поскольку стек — это оперативная память).
В Linux функция, написанная на языке программирования C, будет вызываться из кода ассемблера.
На языке программирования C a struct
может использоваться для доступа к данным, хранящимся в ОЗУ, если известно, как хранятся данные. И поскольку мы знаем, в каком порядке мы написали push
инструкции в нашем коде на ассемблере, мы можем определить struct
, который можно использовать для доступа к данным в стеке:
struct registers {
unsigned long ebx;
unsigned long ecx;
unsigned long edx;
...
unsigned long eax;
unsigned long eip;
...
}
В написанной на C функции в ядре мы теперь можем получить доступ к этой структуре для считывания значений регистра:
void systemCall_4(struct registers * regs)
{
kernelFile * f;
int (*pWrite)(kernelFile *,const void *,int);
/* Get the file from the file handle */
f = getFileFromHandle(regs->ebx);
/* No such file */
if(f == NULL)
{
regs->eax = ERROR_INVALID_HANDLE;
}
/* Call the device driver */
else
{
pWrite = f->writeFunction;
regs->eax = pWrite(f, (const void *)(regs->ecx), regs->edx);
}
}
Программисты ядра решили определить, что ecx
указывает на данные и edx
является длиной.
В MS-DOS (например) все наоборот: ecx
это длина и edx
указывает на данные. Итак, вы видите, что разработчики Linux также могли решить сделать это по-другому.