Assembler - язык неограниченных возможностей
0e1cc9b4

Программирование без использования libc


Может оказаться, что программа вынуждена многократно вызывать те или иные стандартные функции из libc в критическом участке, тормозящем выполнение всей программы. В этом случае стоит обратить внимание на то, что многие функции libc на самом деле всего лишь более удобный для языка С интерфейс к системным вызовам, предоставляемым самим ядром операционной системы. Такие операции, как ввод/вывод, вся работа с файловой системой, с процессами, с TCP/IP и т.п., могут выполняться путем передачи управления ядру операционной системы напрямую.

Чтобы осуществить системный вызов, надо передать его номер и параметры на точку входа ядра аналогично функции libc syscall(2). Номера системных вызовов (находятся в файле /usr/include/sys/syscall.h) и способ обращения к точке входа (дальний call по адресу 0007:00000000) стандартизированы SysV/386 ABI, но, например в Linux, используется другой механизм — прерывание 80h, так что получается, что обращение к ядру операционной системы напрямую делает программу привязанной к этой конкретной системе. Часть этих ограничений можно убрать, используя соответствующие #define, но в общем случае этот выигрыш в скорости оборачивается еще большей потерей переносимости, чем само использование ассемблера в UNIX.

Посмотрим, как реализуются системные вызовы в рассматриваемых нами примерах:

// hellolnx.s // Программа, выводящая сообщение "Hello world" на Linux // без использования libc // // Компиляция: // as -о hellolnx.o hellolnx.s // ld -s -o hellolnx hellolnx.o // .text .globl _start _start: // системный вызов #4 "write", параметры в Linux помещают слева направо, // в регистры %еах, %ebx, %ecx, %edx, %esi, %edi movl $4,%eax xorl %ebx,%ebx incl %ebx // %ebx = 1 (идентификатор stdout) movl $message,%ecx movl $message_l,%edx // передача управления в ядро системы - прерывание с номером 80h int $0x80

// системный вызов #1 "exit" (%еах = 1, %ebx = 0) xorl %eax,%eax incl %eax xorl %ebx,%ebx int $0x80 hlt

.data message: .string "Hello world\012" message_l = . - message


Linux — это довольно уникальный случай в отношении системных вызовов. В более традиционных UNIX-системах — FreeBSD и Solaris — системные вызовы реализованы согласно общему стандарту SysV/386, и различие в программах заключается только в том, что ассемблер, поставляемый с FreeBSD, не поддерживает некоторые команды и директивы.

// hellobsd.s // Программа, выводящая сообщение "Hello world" на FreeBSD // без использования libc // // Компиляция: // as -о hellobsd.o hellobsd.s // ld -s -o hellobsd hellobsd.o // .text .globl _start _start: // системная функция 4 "write" // в FreeBSD номер вызова помещают в %еах, а параметры - в стек // справа налево плюс одно двойное слово pushl $message_l // параметр 4 - длина буфера pushl $message // параметр 3 - адрес буфера pushl $1 // параметр 2 - идентификатор устройства movl $4,%еах // параметр 1 - номер функции в еах pushl %eax // в стек надо поместить любое двойное слово, но мы поместим номер вызова // для совместимости с Solaris и другими строгими операционными системами // lcall $7,$0 - ассемблер для FreeBSD не поддерживает эту команду .byte 0x9a .long 0 .word 7 // восстановить стек addl $12,%esp // системный вызов 1 "exit" xorl %eax,%eax pushl %eax incl %eax pushl %eax // lcall $7,$0 .byte 0x9A .long 0 .word 7 hlt

.data message: .ascii "Hello world\012" message_l = . - message

И теперь то же самое в Solaris:

// hellosol.s // Программа, выводящая сообщение "Hello world" на Solaris/x86 // без использования libc // // Компиляция: // as -о hellosol.o hellosol.s // ld -s -o hellosol hellosol.o .text .globl _start _start: // комментарии - см. hellobsd.s pushl $message_l pushl $message movl $4,%eax pushl %eax lcall $7,$0 addl $16,%esp

xorl %eax,%eax pushl %eax incl %eax pushl %eax lcall $7,$0 hit

.data message: .string "Hello world\012" message_l = . - message

Конечно, создавая эти программы, мы нарушили спецификацию SysV/386 ABI несколько раз, но из-за того, что мы не обращались ни к каким разделяемым библиотекам, это прошло незамеченным. Требования к полноценной программе сильно разнятся в различных операционных системах, и все они выполнены с максимально возможной тщательностью в файлах crt*.o, которые мы подключали в примере с использованием библиотечных функций. Поэтому, если вы не ставите себе цель сделать программу абсолютно минимального размера, гораздо удобнее назвать свою процедуру main (или _main) и добавлять crt*.o и -lс при компоновке.


Содержание раздела