Отправляет email-рассылки с помощью сервиса Sendsay

системеные вызовы в ядре Linux

У нас в городе народ собирается на линуксовках и обсуждает там иногда
очень интересные вопросы. Так вот на прошлой линуксовке у нас возник
спор о системных вызовах ядра. Хотя, он в итоге не к чему не привел,
зато побудил во мне мысли почитать что-нибудь на эту тему - и я почитал.

Итак, на линуксовке один человек утверждал, что одно из преимуществ
linux-ядра перед ядром, используемым в ядре ОС FreeBSD, является то, что
передача параметров при вызовах системных функций происходит через
регистры, а во FreeBSD через стек.

Что ж, интересно, посмотрим так ли дела обстоят на самом деле -
рассмотрим ядро 2.4.27:

Немного теории из прочитанной мной документации:
http://citforum.ru/operating_systems/linux/gdb/
"когда вызывается системный вызов, его ID, помещается в регистр eax и
генерируется программное прерывание (int 0x80). Есть специальный
обработчик прерывания, помещающий этот адрес в таблицу дескрипторов
прерываний и отвечающий за обработку прерывания (снова int 0x80). Затем
вызывается обработчик системных вызовов system_call. Этот обработчик,
зная адрес таблицы системных вызовов и ID системного вызова (который
находится в регистре eax), может определить реальный адрес запрошенного
системного вызова."

http://www.lowlevel.ru/articles/sys_call.htm
"Компилятор, встретив вызов функции, преобразует его в ассемблерный код,
обеспечивая загрузку номера системного вызова, соответствующего данной
функции, и ее параметров в регистры процессора и последующий вызов
прерывания 0x80.
В регистры процессора загружаются следующие значения:
* в регистр EAX - номер системного вызова.
* в регистр EBX - первый параметр функции
* в регистр ECX - второй параметр
* в регистр EDX - третий параметр и т.д.

Для выполнения системного вызова в ОС Linux используется функция
system_call, которая определена в файле arch/i386/kernel/entry.S. Эта
функция - точка входа для всех системных вызовов. Ядро реагирует на
прерывание 0x80 обращением к функции system_call, которая по сути
представляет собой обработчик прерывания 0x80.

. . .

ядро вызывает обработчик прерывания 0x80 - функцию system_call.
System_call помещает копии регистров, содержащих параметры вызова, в
стек при помощи макроса SAVE_ALL, и командой call вызывает нужную
системную функцию. Таблица указателей на функции ядра, которые реализуют
системные вызовы, расположена в массиве sys_call_table (см. файл
arch/i386/kernel/entry.S). Номер системного вызова, который находится в
регистре EAX, является индексом в этом массиве. Таким образом, если в
EAX находится значение 8, будет вызвана функция ядра sys_creat()

Возвращаемое системным вызовом значение сохраняется в регистр EAX."

http://www.linuxshare.ru/docs/devel/syscall_Linux.html
"Зачем нужен макрос SAVE_ALL? Объяснение тут очень простое. Так как
практически все системные функции ядра написаны на C, то свои параметры
они ищут в стеке. А параметры помещаются в стек при помощи SAVE_ALL"

Теперь подтвердим теории практикой.

1. Системный вызов снаружи. При вызове системных функций параметры
передаются в регистрах

Приведем пример рабочей программы на языке ассемблер (компилировать
используя NASM "nasm -felf prog01.asm -o prog01.o" и "ld prog01.o -o
prog01"):

пример взят из http://www.lowlevel.ru/articles/linux_first_prog.htm
begin prog01.asm global _start

_start:

mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, msglen
int 0x80

mov eax, 1
mov ebx, 0
int 0x80

section .data

msg: db "Linux rulez 4ever",0x0A,0
msglen equ $-msg
end prog01.asm Видно, что перед каждым системным вызовом его параметры помещаются в
регистры процессора.

2. Системный вызов внутри. Использование передаваемых параметров.

Скопируем полученное при компиляции не запакованное ядро (файл vmlinux)
в каталог tmp и сделаем его доступным на чтение для обычного
пользователя.

Посмотрим по какому адресу расположена sys_call_table:

$ nm vmlinux | grep sys_call_table
c0240a95 R __kstrtab_sys_call_table
c0248178 R __ksymtab_sys_call_table
c024a270 D sys_call_table

Нас интересует последняя строчка - c024a270. Теперь продолжим работать с
файликом vmlinux в отладчике gdb:

$ gdb -q vmlinux
(gdb)

Дизассембелируем функцию system_call:

(gdb) disass system_call
Dump of assembler code for function system_call:
0xc0108c54 <system_call+0>: push %eax
0xc0108c55 <system_call+1>: cld
0xc0108c56 <system_call+2>: push %es
0xc0108c57 <system_call+3>: push %ds
0xc0108c58 <system_call+4>: push %eax
0xc0108c59 <system_call+5>: push %ebp
0xc0108c5a <system_call+6>: push %edi
0xc0108c5b <system_call+7>: push %esi
0xc0108c5c <system_call+8>: push %edx
0xc0108c5d <system_call+9>: push %ecx
0xc0108c5e <system_call+10>: push %ebx
0xc0108c5f <system_call+11>: mov $0x18,%edx
0xc0108c64 <system_call+16>: mov %edx,%ds
0xc0108c66 <system_call+18>: mov %edx,%es
0xc0108c68 <system_call+20>: mov $0xffffe000,%ebx
0xc0108c6d <system_call+25>: and %esp,%ebx
0xc0108c6f <system_call+27>: testb $0x2,0x18(%ebx)
0xc0108c73 <system_call+31>: jne 0xc0108cd4 <tracesys>
0xc0108c75 <system_call+33>: cmp $0x10e,%eax
0xc0108c7a <system_call+38>: jae 0xc0108d01 <badsys>
0xc0108c80 <system_call+44>: call *0xc024a270(,%eax,4)
0xc0108c87 <system_call+51>: mov %eax,0x18(%esp)
0xc0108c8b <system_call+55>: nop
End of assembler dump.

Ага, вот здесь интересно - параметры переданные в регистрах скидываются
в стек - строки "<system_call+2> - <system_call+10>". В строке
"<system_call+51>" происходит вызов функции по адресу взятому из в
таблицы sys_call_table (видно из того, что совпадают адреса: полученный
нами и дизассемблированный) со смещением в ней, указанном в регистре eax.

В приведенной выше программе на ассемблере происходил вызов системной
функции с номером 4: sys_write - дизассемблируем сейчас ее:

(gdb) disass sys_write
Dump of assembler code for function sys_write:
0xc0138450 <sys_write+0>: sub $0x28,%esp
0xc0138453 <sys_write+3>: mov 0x2c(%esp),%eax
0xc0138457 <sys_write+7>: mov %esi,0x1c(%esp)
0xc013845b <sys_write+11>: mov %edi,0x20(%esp)
0xc013845f <sys_write+15>: mov $0xfffffff7,%edi
0xc0138464 <sys_write+20>: mov %ebp,0x24(%esp)
0xc0138468 <sys_write+24>: mov 0x34(%esp),%ebp
0xc013846c <sys_write+28>: mov %ebx,0x18(%esp)
0xc0138470 <sys_write+32>: call 0xc0139370 <fget>
0xc0138475 <sys_write+37>: test %eax,%eax
0xc0138477 <sys_write+39>: mov %eax,%esi
0xc0138479 <sys_write+41>: je 0xc0138544 <sys_write+244>
0xc013847f <sys_write+47>: movzwl 0x1c(%eax),%eax
0xc0138483 <sys_write+51>: and $0x2,%eax
0xc0138486 <sys_write+54>: test %ax,%ax
0xc0138489 <sys_write+57>: je 0xc0138500 <sys_write+176>
0xc013848b <sys_write+59>: mov 0x8(%esi),%eax
0xc013848e <sys_write+62>: mov 0x20(%esi),%ecx
0xc0138491 <sys_write+65>: mov 0x24(%esi),%ebx
...

В приведенном листинге видно, что тело функции оперирует параметрами
взятыми из стека. Например, дескриптор файла, в который выводится
передаваемая строка, находившийся перед вызовом функции в регистре ebx
(см. листинг ассемблерной программы), затем, переместился в стек (строка
"<system_call+10>" листинга system_call), и, наконец, извлекается из
него обратно - строка "<sys_write+3>".

Какой вывод можно сделать из этого?
Очень странно, то что параметры передаются через регистры, ибо благодаря
макросу SAVE_ALL в обработчике прерывания 0x80 прерывания все равно, эти
регистры сохраняются в стеке, откуда потом уже извлекаются внутри
системного вызова. Хотя все-таки некоторые функции оперируют параметрами
переданными через регистры - взять хотя функцию fget, которая вызывается
в "<sys_write+32>". Вот что выдал gdb:

(gdb) disass fget
Dump of assembler code for function fget:
0xc0139370 <fget+0>: mov %eax,%ecx
0xc0139372 <fget+2>: mov $0xffffe000,%eax
0xc0139377 <fget+7>: and %esp,%eax
0xc0139379 <fget+9>: mov 0x554(%eax),%edx
0xc013937f <fget+15>: xor %eax,%eax
0xc0139381 <fget+17>: cmp 0x4(%edx),%ecx
0xc0139384 <fget+20>: jae 0xc013938c <fget+28>
0xc0139386 <fget+22>: mov 0x10(%edx),%eax
0xc0139389 <fget+25>: mov (%eax,%ecx,4),%eax
0xc013938c <fget+28>: test %eax,%eax
0xc013938e <fget+30>: je 0xc0139393 <fget+35>
0xc0139390 <fget+32>: incl 0x14(%eax)
0xc0139393 <fget+35>: ret

Здесь нет явного использования параметра, взятого из стека (по крайней
мере я этого не увидел), а есть использование регистра eax в <fget+0>,
который, судя по всему, должен был иметь какое-то значение перед
вызовом функции.

В связи с этим прошу объяснить мне, что происходит с вызовами на самом
деле, если я что-то не доглядел или не допонял...

Ответить   Tue, 29 Mar 2005 13:48:48 +0400 (#341738)

 

Ответы:

Приветствую!

В Втр, 29.03.2005, в 13:48, Kolotov Alexandr пишет:


Возможно я неправ, но судя по всему все происходит где-то так:

При вызове INT 0x80 из третьего кольца в первое происходит смена стека
(IA-32 Software Developer Manual, томик третий (253668), страницы 5-16,
5-17)! На новый стек кладутся SS, ESP, EFLAGS, CS, EIP. А отсюда следует
одно - для доступа к тем данным, что в FreeBSD передаются через стек, в
обработчике необходимо поднять из стека ESP и SS, сходить взять данные и
переписать их в новый стек, и только после этого пнуть обработчик
конкретного вызова. В случае Linux к нам все пришло в регистрах, мы
спокойно пихаем все в стек и вызываем (call) обработчик конкретного
вызова.

Почему пихаем в новый стек? Так должно быть согласно ABI C для 386
процессоров. Передача параметров в функции через регистры - это
отклонение от стандартного ABI, которое, тем не менее, можно успешно
использовать (-mregparm={1,2,3} в GCC). И если ядро компилировалось с
включенной опцией передачи параметров функций через регистры, то такая
штука с fget неудивительна.

--
Roman.
http://www.3os.ru/ http://www.osrc.info/
gpg --recv-keys 0xE5E055C3

-*Название листа "Linux: разрешение вопросов, перспективы и общение";
Написать в лист: mailto:comp.soft.linux.discuss-list@subscribe.ru
Адрес правил листа http://subscribe.ru/catalog/comp.soft.linux.discuss/rules
Номер письма: 17370; Возраст листа: 613; Участников: 1353
Адрес сайта рассылки: http://www.linuxrsp.ru
Адрес этого письма в архиве: http://subscribe.ru/archive/comp.soft.linux.discuss/msg/341920

Ответить   Roman I Khimov Tue, 29 Mar 2005 18:41:29 +0400 (#341920)

 

В сообщении от 1112093328 секунд после начала Эпохи Unix Вы написали:

Вообще-то есть специальный параметр конфигурации `CONFIG_REGPARM', при
включении которого параметры внутри ядра передаются через регистры.
Например, у меня он включен. Если подскажите как из сжатого ядра сделать
несжатое (компилировать ядро заново не имею желания, так как на моей машине
это происходит довольно долго), то могу показать код тех же system_call и
sys_write.

Ответить   Konstantin Korikov Tue, 29 Mar 2005 16:10:57 +0300 (#342056)

 

Здравствуйте, Konstantin.

Вы писали 29 марта 2005 г., 17:10:57:

initrd - gzip запакован.
kenrel - вроде как образ дискетки???

Ответить   "Kanogin A.A." Tue, 29 Mar 2005 22:48:41 +0400 (#342065)

 

В сообщении от 1112125721 секунд после начала Эпохи Unix Вы написали:

и

initrd у меня вообще не используется (да он не нужен для данной задачи).
Образ ядра сжат, но не полностью. В начале расположен код, который
распаковывает оставшуюся часть, и делает переход не определенный адрес
в ней.

Ответить   Konstantin Korikov Wed, 30 Mar 2005 00:59:43 +0300 (#342144)