системеные вызовы в ядре 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>,
который, судя по всему, должен был иметь какое-то значение перед
вызовом функции.
В связи с этим прошу объяснить мне, что происходит с вызовами на самом
деле, если я что-то не доглядел или не допонял...
Приветствую!
В Втр, 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