Как этот вызов ядра узнает, что нужно захватить из регистра счетчика

#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 также могли решить сделать это по-другому.