Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
NEWS
Последние статьи :
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
  SQL 30.07   
  JFS 10.06   
  B-trees 01.06   
 
TOP 20
 Go Web ...603 
 2.0-> Linux IP Networking...354 
 Secure Programming for Li...294 
 2.6-> Kernel 2.6.17...226 
 William Gropp...222 
 Kamran Husain...221 
 Robbins 4...218 
 Advanced Bash Scripting G...211 
 Rodriguez 9...197 
 Rodriguez 6...195 
 Ethreal 1...193 
 UML 3...192 
 Стивенс 8...190 
 Daniel Bovet 2...189 
 Steve Pate 1...187 
 Steve Pate 3...186 
 Kamran Husain...184 
 Advanced Bash Scripting G...182 
 Rodriguez 8...179 
 Kernel Notes...176 
 
  01.03.2019 : 2682145 посещений 

iakovlev.org
Ассемблер, который я буду использовать - NASM (Netwide Assembler, nasm.2y.net). Этот выбор объясняется тем, что:
Во первых, он мультиплатформенный, т.е. для портирования программы на разные ОС достаточно только изменить код взаимодействия с системой, а всю программу переписывать не нужно
Во вторых, он, его синтаксис непротиворечив и недвусмысленен, в чем схож с AT&T ассемблером для UNIX
В третьих, он имеет привычный Intel-синтаксис, т.е. программист на MASM или TASM сможет без особых проблем перейти на NASM

А теперь перейдем к первой программе:

 ;Листинг 01 - минимальная программа для Linux
 ;Приемы оптимизации не применяются для упрощения кода
 
 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
 
 

Рассмотрим программу поподробнее:
Знак ';' (точка с запятой) означает комментарий - все что находится правее этого символа ассемблер игнорирует

global _start - директива global указывает ассемблеру сделать глобальной (экспортируемой) метку "_start". Подробнее об экспортируемых метках см. ниже

_start: - объявление метки с именем "_start". Фактически это означает, что в программе будет определена константа _start, которая будет иметь значение равное адресу, по которому объявлена данная метка

Предыдущие три строчки были директивами ассемблера, т.е. не являлись командами процессора, и не преобразовывались при компиляции в машинный код. Следущие строчки являются именно командами процессора:

mov eax, 4 - машинная команда MOV копирует данные из второго операнда в первый. В данном случае первый операнд - это регистр EAX (подробнее о регистрах - в следующем уроке). Второй операнд - это константа (определенное в момент компилирования и неизменяемое значение). Результатом выполнения этой команды будет то, что в регистре EAX окажется число 4. Операнды команды разделяются запятой

mov ebx, 1 - то же самое, но помещается единица в регистр EBX

mov ecx, msg - на первый взгляд эта команда отличается от двух предыдущих, но она тоже выполняет перемещение данных, только в данном случае используется константа msg, которая определена ниже и регистр ECX

mov edx, msglen - содержимое определенной ниже константы msglen помещается в регистр EDX

int 0x80 - команда int процессора вызывает т.н. программное прерывание. Грубо говоря - программное прерывание - это команда перехода выполнения программы в определенный операционной системой обработчик прерывания. Всего процессор поддерживает 256 обработчиков для 256 прерываний и операнд этой команды указывает на обработчик какого прерывания нужно передать выполнение программы. 0x80 - 80 в шестнадцатеричной системе счисления (на шестнадцатеричную систему указывают первые два символа: 0x). В случае ОС Linux, прерывание с номером 0x80 является системным вызовом - передачей управления ядру системы с целью выполнения каких-либо действий. В регистре EAX должен находится номер системного вызова, в зависимости от которого ядро системы будет выполнять какие-либо действия. В данном случае мы помещаем в EAX число 4, т.е. указываем ядру выполнить системный вызов номер 4 (write). Этот системный вызов используется для записи данных в файл или на консоль (которая тоже в принципе представлена файлом). В EBX мы поместили дескриптор(идентификатор) консоли - stdout. В ECX и EDX содержатся адрес начала сообщения (адрес первого байта) и длина сообщения в байтах. Т.е этот системный вызов должен выполнить вывод строчки, находящейся по адресу msg, на консоль.

mov eax, 1 - в EAX помещается 1 - номер системного вызова "exit"

mov ebx, 0 - в EBX помещается 0 - параметр вызова "exit" означает код с которым завершится выполнение программы

int 0x80 - системный вызов. После системного вызова "exit" выполнение программы завершается

section .data Директива ассемблера section определяет следующие данные, как находящиеся в указанном в качестве параметра сегменте. Сегмент .text - сегмент кода, в котором должен находиться исполняемый код программы и чтение из которого запрещено. Сегмент .data - сегмент данных, в котором должны находиться данные программы. Выполнение (передача управления) на сегмент данных запрещена. Поскольку следующие строчки нашей программы - данные, то мы определяем сегмент данных.

msg: db "Linux rulez 4ever",0x0A,0 - вначале мы определяем метку msg (напоминаю, что метка - текущий адрес), и сразу после нее - строчку, т.е. метка msg будет указывать на первый байт строки. Директива db указывает ассемблеру поместить в данном месте байт данных. Несколько байт могут быть разделены запятой. Если нужно поместить символ, то запись 'X' означает код символа 'X', а форма записи "abcde" эквивалентна 'a', 'b', 'c', 'd', 'e'. Код символа 0x0A означает переход строки, а нулевой байт является концом строки. Поскольку вызов write знает точно, сколько байт нужно выводить, то нулевой байт в конце строки необязателен, но мы его все равно поставим :). Он необходим для программ, взаимодействующих с GLIBC, т.к. функции стандартной библиотеки Си вычисляют длину строки, как расстояние между первым байтом и ближайшим нулевым байтом.

msglen equ $-msg - директива equ определяет константу, расположенную слева от директивы и присваивает ей значение, находящееся справа. Символ $ является специальной константой ассемблера, значение которой всегда равно адресу по которому она находится, т.е в данном случае выражение $ - msg как раз будет равно длине строки, т.к. в данном месте программы $ равно адресу следующего за строкой байта. Результат этой директивы - мы определили константу msglen, значение которой равно длине определенной выше строки.

Результат работы ассемблера - это объектный файл. Так как мы компилируем программу под Linux, то нам необходим объектный файл формата ELF (Executable and Linkable Format). Получить его можно следующей командой:
nasm -felf prog01.asm -o prog01.o
Полученный объектный файл необходимо скомпоновать. Такое название это действие получило потому, что с его помощью можно компоновать несколько объектных файлов в один исполняемый. Если в каком-нибудь из объектных файлов существуют экспортируемые функции или переменные, то они доступны всем компонуемым объектным файлам. Существует функция, которая должна быть определена всегда - это точка входа - "_start". С этой функции начинается выполнение программы.
Компоновка:
ld prog01.o -o prog01
Поскольку мы не использователи никаких библиотек, а взаимодействовали напрямую с ядром системы, то при компоновке мы указываем только наш объектный файл.
После выполнения этой команды файл "prog01" будет исполняемым файлом нашей программы.


GLIBC - стандартная библиотека Си от GNU. Если вы программируете на ассемблере под Linux, то использование функций из этой библиотеки - хороший способ сократить размер программы и затраченные усилия. Безусловно, использование их замедляет программу, но это всего лишь значит, что их не стоит использовать в критических участках - циклах. Если же вы используете GLIBC скажем для форматированного вывода на консоль, то вряд ли вы заметите какое-нибудь замедление.

Более того - использование GLIBC в большинстве случаев сделает вашу программу легко портируемой на многие другие UNIX-платформы.

В качестве примера рассмотрим программу, которая импортирует функцию puts (вывод на консоль null-terminated строки)

;Точка входа "_start" на самом деле находится
 ;в подключаемом *.o файле стандартной библиотеки Си
 ;Она передает управление на функцию "main",
 ;которая должна находиться в нашей программе
 global main
 
 ;Внешние функции
 extern exit
 extern puts
 
 ;Сегмент кода:
 section .text
 
 ;Функция main:
 main:
 
 ;Параметры передаются в стеке:
 push dword msg
 call puts
 
 ;По конвенции Си вызывающая процедура должна
 ;очищать стек от параметров самостоятельно:
 sub esp, 4
 
 ;Завершение программы с кодом выхода 0:
 push dword 0
 call exit
 
 ret
 
 ;Сегмент данных
 section .data
 
 msg: db "An example of interfacing with GLIBC.",0x0D,0
 
 
Компиляция:
nasm -felf inglibc.asm
Компоновка:
Для вызова компоновщика с нужными параметрами мы не будем заморачиваться с командой ld, а воспользуемся GCC, который сам определит, что нужно нашему объектному файлу:
gcc inglibc.o -o inglibc



Разделяемые объекты (shared objects) в Linux являются аналогами .DLL в Windows. Находятся они обычно в /usr/lib и имеют расширение .so. Что они из себя представляют? Это исполняемые файлы формата ELF, которые экспортируют некоторые функции.

В качестве примера создадим библиотеку chomp.so, которая будет экспортировать функцию chomp (отрезание последнего символа строки, если это символ новой строки '\n')

;Экспортирование функцию chomp:
 global chomp
 
 ;Объявление функции chomp:
 chomp:
 
 ;В качестве параметра функция берет строку
 ;(точнее указатель на нее)
 ;Первые четыре байта - адрес возврата,
 ;значит нам нужны вторые четыре байта
 mov eax, [esp+4]
 ;Теперь в EAX адрес строки
 
 xor ecx, ecx
 
 ;Цикл - поиск нулевого символа (конца строки):
 
 .loop
 mov dl, [eax+ecx] ;Символ - в DL
 inc ecx	;Увеличим счетчик цикла
 cmp dl, 0 ;Если не 0
 jne .loop ;То вернуться в начало цикла
 
 ;Уменьшение ECX на 2:
 dec ecx
 dec ecx
 
 ;Последний символ строки поместим в DL:
 mov dl, [eax+ecx]
 
 ;Если это не символ новой строки:
 cmp dl, 0x0A
 ;То выйти
 jne .quit
 
 ;иначе отрезать его
 ;(поместить на его место символ конца строки)
 mov [eax+ecx], byte 0
 
 .quit:
 
 ;Завершение функции
 ret
 
 
 

Компиляция:
nasm -felf chomp.asm -o chomp.o

Компоновка:
ld chomp.o -shared -o chomp.so



Системный вызов Linux "read" (#3) предназначен для чтения из файла с текущей позиции. Также он может быть использован для чтения данных введенных с клавиатуры (используется файловый дескриптор 02 - stdin).

Ниже приведена программа, которая выведет введенные с клавиатуры символы на экран.

global _start
 
 _start:
 
 mov eax, 3	;Вызов #3
 mov ebx, 2	;Дескриптор stdin
 mov ecx, buffer	;Адрес буфера для хранения введенных данных
 mov edx, 10	;Максимальная длина ввода
 int 0x80	;Прерывание - системный вызов
 
 mov eax, 4	;Вызов #4 (write)
 mov ebx, 1	;Дескриптор stdout
 
 ;Системный вызов не изменил содержимое регистров ECX и EDX
 ; поэтому следующие две строчки не нужны
 ;mov ecx, buffer;Адрес строки для вывода
 ;mov edx, 10	;Длина выводимых данных
 
 int 0x80	;Системный вызов
 
 xor eax, eax	;Обнуление регистра EAX
 inc eax		;Инкремент - увеличение на единицу
 int 0x80	;Системный вызов
 
 section .data	;Начало сегмента данных
 buffer: resb 10	
 
 
 
Директива ассемблера resb 10 предназначена для резервирования указанного количества байт. Содержимое этих байт не определено, но поскольку они находятся в сегменте данных, то их содержимое будет равно нулю.

Команда xor операнд1, операнд2 на самом деле выполняет логическую операцию "исключающее или" над каждым битом операндов, т.е. какой-либо бит результата равен 1 только в том случае, если значения соотвествующих битов операндов различны. Эта операция чаще всего используется для обнуления регистров - очевидно, что если операнды равны, то все биты результата будут равны 0. Команды inc операнд увеличивает содержимое операнда на единицу. Для занесения единицы в регистр лучше использовать не mov reg, 1, а последовательность команд:
xor reg, reg
inc reg
поскольку команда mov в четырехбайтный регистр занимает пять байт, а указанная выше последовательность - только 3 байта. Аналогичным образом для занесения в регистр двойки лучше воспользоваться командой xor и дважды применить команду inc - это займет четыре байта





Иногда (особенно часто это случается при разработке ОС) перед программистом встает задача обеспечения взаимодействия между различными модулями, одна часть которых написана на ассемблере для повышения быстродействия, а другая - на Си (или каком-нибудь другом высокоуровневом языке программирования). Взаимодействие между ними (скомпилированными как разные объектные файлы) осуществляется следующим образом (я покажу на примере NASM и GCC):
Для того чтобы функция, написанная на NASM стала доступна из GCC, ее необходимо объявить глобальной:
global function_name
Если же программа на ассемблере использует какую-нибудь функцию экспортируемую из модуля, написанного на Си, то ее необходимо объявить внешней:
extern function_name

Конвенции вызова функций

Конвенция вызова функций используемая в Си предполагает передачу аргументов в стеке в обратном порядке, т.е., например, вместо
printf("%i",value);
на ассемблере необходимо написать:
push dword value
push dword format
call printf
К тому же вызванная функция не очищает стек от параметров, поэтому это должна сделать вызывающая функция, например:
add esp, 8 - если были переданы два параметра

Доступ к параметрам

Если функция, написанная на ассемблере, была вызвана из программы, написанной на Си, то доступ к переданным параметрам можно получить следующим образом:
push ebp - EBP будет использоваться
mov ebp, esp - сохранить значение ESP
mov eax, [ebp+8] - для того, чтобы запросить последний параметр из списка, к нему надо обратиться как к ebp+8 (первые четыре байта в стеке - это адрес возврата, помещенный туда командой call, а вторые четыре байта - это сохраненный в начале функции регистр EBP), для получения второго - ebp+12 и т.д.
Значение esp необходимо сохранять, потому что в процессе исполнения функции оно может меняться
Такая функция должна завершиться командами pop ebp и ret

В некоторых форматах объектных файлов, компилятор будет добавлять подчеркивание к адресу функции, поэтому чтобы функция, написанная на ассемблере, была доступна как function_name, ее необходимо объявить как _function_name



Ядро Linux предоставляет системный вызов #2 fork для "ветвления" процесса. Этот системный вызов создает дочерний процесс, который отличается от создавшего его только идентификатором процесса. Дочерний процесс получает память родительского, причем используется метод COW - copy on write (копирование при записи), т.е. память действительно копируется только тогда, когда в нее производится запись, а до этого таблицы страниц обеих процессов указывают на одну и ту же область памяти

Как программе отличить в каком из процессов она выполняется? Очень просто: родительскому процессу fork возвращает PID дочернего, а дочернему возвращает 0.

Рассмотрим программу, которая разветвится на две части с помощью системного вызова fork. Одна из частей получит управление в родительском процессе, вторая - в дочернем

global _start
 
 _start:
 
 ;Системный вызов #2 fork:
 mov eax, 2
 int 0x80
 
 ;Проверка возвращаемого значения
 test eax, eax
 ;Если ноль - то это дочерний процесс:
 jz child
 ;Иначе - это родительский процесс:
 mov eax, 4
 mov ebx, 1
 mov ecx, msg1
 mov edx, msg1len
 int 0x80
 
 jmp short quit
 child:
 mov eax, 4
 mov ebx, 1
 mov ecx, msg2
 mov edx, msg2len
 int 0x80
 
 quit:
 mov eax, 1
 int 0x80
 
 section .data
 msg1: db "I am the parent process",0x0A
 msg1len equ $-msg1
 msg2: db "I am the child process",0x0A
 msg2len equ $-msg2
 

Невозможно предсказать, какая из надписей появится первой - та, которая выводится родительским процессом или та, которая выводится дочерним. Запустите программу несколько раз и вы увидите, что в появлении надписей нет единого порядка. Действительно - для ядра системы эти две ветки стали различными процессами и порядок их выполнения уже на совести менеджера процессов.

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье