Написание собственного прерывания в Linux


    Как известно, при программировании на ассемблере в Линуксе используется лишь одно прерывание int 0x80, предоставляющее доступ к системным вызовам (номер которых указывается в регистре %eax). В данной статье демонстрируется способ написания пользовательского прерывания int 0x85, предоставляющего доступ к дополнительному набору функций. Работа велась в системе Fedora Core 3, ядро 2.6.9.

    Для испытаний были взяты строковые функции библиотеки string - как известно, функции этой библиотеки автономны, т.е. не обращаются к функциям извне, что особенно удобно для нашего эксперимента. Чтобы не мудрствовать лукаво, я просто взял файл string.c из директории исходников /usr/src/linux-custom/lib (наши исходники находятся в папке /usr/src/linux-custom -  впредь этот префикс мы будем опускать). Вам надо только будет переименовать все функции из файла, добавив к ним какой-нибудь префикс, напр., "lib_" (чтобы они не путались с внутренними функциями ядра), да и сам файл переименовать в lib_string.c, записав его в папку arch/i386/kernel

    Рассмотрим ссылки на системные вызовы, перечисленные в конце файла arch/i386/kernel/entry.S:

.data
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */ 
...
.long sys_mq_timedreceive /* 280 */
.long sys_mq_notify
.long sys_mq_getsetattr
.long sys_ni_syscall /* reserved for kexec */
.long sys_waitid 

syscall_table_size=(.-sys_call_table)
 

    Каждое имя функции представляет из себя переменную типа .long (4 байта в архитектуре i386). Обратим внимание на переменную syscall_table_size - она содержит размер таблицы прерываний. Точка "." означает оффсет текущей позиции от начала файла и соответствует символу '$' в досовском ассемблере. Метка в ассемблеровой программе, как известно, будучи взята как переменная, имеет в качестве значения свой оффсет. Размер таблицы вычисляется путём вычитания метки начала таблицы из метки её конца.

    Код функций системных вызовов разбросан по многим файлам в разных папках, но это уже забота makefile'а - он сам найдёт и наш "доморощенный" файл lib_string.c. Мы, конечно, могли бы упростить себе задачу и дописать наши функции вслед за стандартными системными вызовами:

.long lib_strnicmp /* 0 */ /* 285 */
.long lib_strcpy
.long lib_strncpy
.long lib_strlcpy
.long lib_strcat /* 290 */
.long lib_strncat /* 5 */
.long lib_strlcat
.long lib_strcmp
.long lib_strncmp
.long lib_strchr /* 295 */
.long lib_strrchr /* 10 */
.long lib_strnchr
.long lib_strspn
.long lib_strlen
.long lib_strcspn /* 300 */ 

syscall_table_size=(.-sys_call_table)


    Тогда для их вызова достаточно было бы поместить в регистр %eax их номер в таблице прерываний - т.е. 285 для lib_strnicmp, 286 - для lib_strcpy и тд. Этот  способ описан более подробно в статье Uncle Bob'а "Создание нового системного вызова в ОС Linux". Но этот вариант нам не интересен - процесс пересмотра списка  системных вызовов идёт постоянно, и никто не даёт гарантии, что когда-нибудь новый стандартный вызов ядра не займёт номер вашего - что тогда, его заново переопределять? Это неудобно, для чего мы и попытаемся создать новое прерывание int 0x85, к которому и привяжем все наши функции. Номер 0х85 произволен - в  Линуксе вообще номера прерываний, отличные от 0х80, при программировании на ассемблере не задействованы. Но номер 0х81, возможно, используется некоторыми отладчиками ядра (по кр. мере мне так показалось), поэтому было решено оставить пустым некоторый промежуток и остановиться на числе 0х85.

    Процедура создания нового прерывания не так проста, т.к. нам придётся научить систему различать два вектора прерываний - помимо SYSCALL_VECTOR ещё и наш собственный вектор, который мы по аналогии назовём LIBCALL_VECTOR. Для этого нам потребуется изменить следующие файлы: 

include/asm-i386/mach-default/irq_vectors.h
arch/i386/pci/irq.c
arch/i386/kernel/io_apic.c
asm/i386/kernel/i8259.c
arch/i386/kernel/traps.c
arch/i386/kernel/entry.S

    Я не владею мастерством создания patch-файлов, поэтому пишу как есть. 

В файле include/asm-i386/mach-default/irq_vectors.h после определения SYSCALL_VECTOR запишем строку: 

#define LIBCALL_VECTOR 0x85

В файле arch/i386/pci/irq.c строки:

#else
if (next == SYSCALL_VECTOR)
continue;
#endif 


меняем на:

#else
if ( (next == SYSCALL_VECTOR) || (next == LIBCALL_VECTOR) )
continue;
#endif 


В файле arch/i386/kernel/io_apic.c строки

next:
current_vector += 8;
if (current_vector == SYSCALL_VECTOR)
goto next;


меняем на:

next:
current_vector += 8;
if ( (current_vector == SYSCALL_VECTOR) || (current_vector == LIBCALL_VECTOR) )
goto next;


В файле asm/i386/kernel/i8259.c строки:

if (vector != SYSCALL_VECTOR) 
set_intr_gate(vector, interrupt[i]); 

меняем на: 

if ( (vector != SYSCALL_VECTOR) || (vector != LIBCALL_VECTOR) )
set_intr_gate(vector, interrupt[i]);


В файле arch/i386/kernel/traps.c заменим:

asmlinkage int system_call(void); 

на:

asmlinkage int launch_system_call(void);
asmlinkage int launch_lib_call(void); 

а в конце файла строку:

set_system_gate(SYSCALL_VECTOR,&system_call);

заменим на две:

set_system_gate(SYSCALL_VECTOR,&launch_system_call); 
set_system_gate(LIBCALL_VECTOR,&launch_lib_call);
 

Последние два выражения связывают номер прерывания (определённого в файле include/asm-i386/mach-default/irq_vectors.h) с процедурой обращения к соответствующему вектору.

    Самым сложным будет редактирование файла arch/i386/kernel/entry.S. Мы создадим отдельную таблицу вектора прерывания int 0x85 libcall_table, т.е., говоря человеческим языком, список ссылок на функции этого прерывания. Далее наша задача будет заключаться в том, чтобы распределить вызовы функций в зависимости от номера прерывания: функции вызываются по номеру, лежащему в регистре %eax - а нам ещё надо указать ядру, к какому вектору относится данный номер, SYSCALL или LIBCALL. Для этого вводится флаг $CALL_FLAG - если при проверке он равен нулю, то вызывается функция из списка SYSCALL, если единице - то LIBCALL (заметим, что $CALL_FLAG - 32-разрядная переменная, а не однобитная! теоретически его можно использовать для различения бóльшего количества прерываний). Я допускаю, что мой код не идеален, так что если кто его усовершенствует, буду только рад. 

Итак, в начале файла после определения переменной nr_syscalls добавим строку:

define nr_libcalls ((libcall_table_size)/4)  

libcall_table_size - длина таблицы, nr_libcalls - количество прерываний, т.е. длина таблицы, делённая на 4 (см. выше)

Теперь отредактируем процедуру sysenter_entry. Символ <...> означает, что несколько пропущенных после метки строк можно не трогать:

ENTRY(sysenter_entry)   # исходный файл
<...>
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry

 



cmpl $(nr_syscalls), %eax

jae syscall_badsys

 

 





call *sys_call_table(,%eax,4)

 

movl %eax,EAX(%esp)
cli

<...> 

ENTRY(sysenter_entry)   # исправленный файл
<...>
   testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
            jnz syscall_trace_entry


              pushl %esi 
              movl (CALL_FLAG),%esi 
              testl %esi,%esi 
              jnz next1 
              cmpl $(nr_syscalls),%eax
              popl %esi
              jae syscall_badsys
              jmp next2
next1:        cmp $1,%esi 
              popl %esi  
              jnz syscall_badsys
              cmpl $(nr_libcalls),%eax 
              jae syscall_badsys 
next2:        pushl %esi
              movl (CALL_FLAG),%esi
              testl %esi,%esi
              jnz next3
              popl %esi 
              call *sys_call_table(,%eax,4)
              jmp next4 
next3:        popl %esi                    # !!! no cmpl $1,%esi
              call *lib_call_table(,%eax,4)

next4:        movl %eax,EAX(%esp)
              cli
<...> 

Назначение вышеописанной процедуры - проверка номера функции прерывания. Если он превышает количество прерываний, то вызывается ошибка и управление передается на метку syscall_badsys. Задача затрудняется тем, что у нас два вектора с различным количеством функций, т.е. нужно проверять номер функции и с nr_syscalls, и с nr_libcalls. Сохранившиеся строки исходного кода выделены жирным шрифтом.

Далее мы добавляем 2 процедуры:

ENTRY(launch_system_call)
movl $0,(CALL_FLAG)
jmp system_call

ENTRY(launch_lib_call)
movl $1,(CALL_FLAG)
jmp system_call 

Они вызываются из файла traps.c (см. выше).

Теперь рассмотрим процедуру system_call, где нам опять надо будет выполнить работу по различению векторов:

ENTRY(system_call)
<...>
jnz syscall_trace_entry

 


cmpl $(nr_syscalls), %eax

jae syscall_badsys




syscall_call:




call *sys_call_table(,%eax,4)


movl %eax,EAX(%esp) # store the return value 

syscall_exit:
cli

ENTRY(system_call)
<...>
           jnz syscall_trace_entry
           pushl %esi 
           movl (CALL_FLAG),%esi 
           testl %esi,%esi 
           jnz next5 
           cmpl $(nr_syscalls),%eax 
           popl %esi
           jae syscall_badsys
           jmp syscall_call
next5:     cmpl $1,%esi 
           popl %esi 
           jnz syscall_badsys
           cmpl $(nr_libcalls),%eax
           jae syscall_badsys

syscall_call:
            pushl %esi
            movl (CALL_FLAG),%esi
            testl %esi,%esi 
            jnz next6 
            popl %esi
            call *sys_call_table(,%eax,4) 
            jmp next7 
next6:      cmpl $1,%esi 
            popl %esi 
            call *lib_call_table(,%eax,4)
next7:      movl %eax,EAX(%esp) # store the return value

syscall_exit:
cli
 

То же самое нам придётся проделать - уже в последний раз! - и с процедурой syscall_trace_entry:

syscall_trace_entry:
<...>
movl ORIG_EAX(%esp), %eax

 

cmpl $(nr_syscalls), %eax

 



jnae syscall_call
jmp syscall_exit

# perform syscall exit tracing
ALIGN

syscall_trace_entry:
<...>
                   movl ORIG_EAX(%esp), %eax
                   pushl %esi
                   movl (CALL_FLAG),%esi
                   testl %esi,%esi 
                   jnz next8 
                   cmpl $(nr_syscalls), %eax
                   popl %esi 
                   jmp next9
next8:             cmpl $1,%esi 
                   popl %esi 
                   jnz syscall_badsys 
                   cmpl $(nr_libcalls), %eax 
next9:             jnae syscall_call 
                 jmp syscall_exit

# perform syscall exit tracing
ALIGN

Вы можете спросить: а нельзя ли было вместо практически трёх одинаковых вышеописанных манипуляций поместить ссылки на одну подпрограмму? Пробовал, не получилось, всё-таки количество свободных регистров почти исчерпано, поэтому чтобы не усложнять код, решил действовать таким простецким способом.

    Ну и наконец перед таблицей sys_call_table (а можно и после оной) помещаем нашу lib_call_table:

ENTRY(lib_call_table)

.long lib_strnicmp /* 0 */
.long lib_strcpy
.long lib_strncpy
.long lib_strlcpy
.long lib_strcat
.long lib_strncat /* 5 */
.long lib_strlcat
.long lib_strcmp
.long lib_strncmp
.long lib_strchr
.long lib_strrchr /* 10 */
.long lib_strnchr
.long lib_strspn
.long lib_strlen
.long lib_strcspn
.long lib_strpbrk /* 15 */
.long lib_strsep
.long lib_strpbrk
.long lib_strsep
.long lib_memset
.long lib_bcopy /* 20 */
.long lib_memcpy
.long lib_memmove
.long lib_memcmp
.long lib_strstr 
.long lib_memscan /* 25 */
.long lib_memchr 

libcall_table_size=(.-lib_call_table)

    Всё готово, теперь можно компилировать ядро. Вносим соответствующие изменения в grub.conf (или lilo.conf), перезагружаемся, выбираем новое ядро и входим в систему. Теперь нам предстоит проверить работу прерывания. Для этого хорошо бы вооружиться каким-нибудь ассемблеровым отладчиком. Не имея ничего против gdb, я рекомендую программу ald, напоминающую досовскую утилиту debug. Наберём следующую программу и откомпилируем:

.data
src: .string "asasas\n\0"

.text
.globl _start

_start:
             movl $13,%eax # strlen
             movl $src,%ebx
             int $0x85 

             movl $1,%eax # exit
             movl $0,%ebx
             int $0x80 

    Запустим программу через отладчик - после команды int $0x85 в регистре %eax должна появиться длина строки src - 7. На первых порах я забывал указывать в определении строки символ её конца '\0' и случалось, что после этой команды компьютер наглухо вис. Правда, впоследствии такие промахи не приводили к столь катастрофическим последствиям, и дело ограничивалось предупреждениями системы об ошибке в программе. Но в любом случае, занимаясь столь серьёзными изменениями ядра, будьте бдительны и готовьтесь к возможным форс-мажорным исходам.

    Следующая программа демонстрирует действие функции strcpy, а также содержит процедуру prreg, которая помогает вывести содержимое регистра на экран:

.data
str1: .ascii "12345\n\0"
str2: .ascii "abcde\n\0"
buf: .ascii "abcdefgh\n"

.text
.globl _start
_start:
             movl $1,%eax # strcpy
             movl $str1,%ebx
             movl $str2,%ecx
             int $0x85

             movl $4,%eax # write
             movl $1,%ebx
             movl $str1,%ecx
             movl $6,%edx
             int $0x80

             call prreg
             movl $4,%eax  # write
             movl $1,%ebx
             movl $str2,%ecx
             movl $6,%edx
             int $0x80

exit:        movl $1,%eax
             movl $0,%ebx
             int $0x80

prreg:       movl %eax,%edx
             movl $8,%ecx
             movl $8,%ebx
back:        movb %dl,%al
             andb $0x0f,%al
             call asc
             dec %ebx
             movb %al,buf(%ebx)
             shr $4,%edx
             loop back

             movl $4,%eax
             movl $1,%ebx
             movl $buf,%ecx
             movl $9,%edx
             int $0x80
             ret

asc:         cmp $0xa,%al
             sbb $0x69,%al
             das
             ret

    Итак, мы научились создавать собственный вектор прерывания и связывать его с теми или иными функциями - новыми системными вызовами. Теоретически можно было бы попытаться вместить в ядро основные наборы функций библиотеки glibc - string, stdio, stdlib и тд. Тогда ассемблер превратился бы в перспективнейший язык для разработки приложений для Линукса, т.к. размер бинарников в десятки раз был бы меньше соответствующих программ из С, а "Linux assembly" перестала бы быть бледной копией использования ассемблера для MS-DOS. Но помимо того, что в мире юниксов приветствуется написание переносимого кода (из-за чего делается выбор в пользу высокоуровневых языков вроде С, а не низкоуровневого ассемблера), мы чрезмерно увеличили бы нагрузку на ядро, что привело бы к снижению общей производительности ядра, т.к. системный вызов - это "дорогостоящая" процедура. Для простых операций типа strlen или strcmp вполне можно воспользоваться userspace, не засоряя пространство ядра. Это следует иметь в виду при написании собственных прерываний - мы пользовались библиотекой string исключительно в учебных целях. Тем не менее, для познавательских целей было бы интересно benchmark-сравнение производительности таких бинарников, использующих вместо тех или иных функций glibc заменяющие их системные вызовы (разумеется, имеются в виду те функции библиотеки glibc, которые сами не содержат обращения к системным вызовам).

В начало сайта

Обсудить



Hosted by uCoz