Команды ассемблера
Значит, sizeof(dev_t) = 8.
Мы бы могли и дальше продолжать искать, но в реальности всё немного по-другому. Если вы посмотрите на определение struct stat (cpp /usr/include/sys/stat.h | less), вы увидите поля с именами __pad1, __pad2, __unused4 и другие (зависит от системы). Эти поля не используются, они нужны для совместимости, и поэтому в man они не описаны. Так что самый верный способ не ошибиться - это просто попросить компилятор Си посчитать это смещение для нас (вычитаем из адреса поля адрес структуры, получаем смещение):
#include <sys/types.h > #include <sys/stat.h > #include <unistd.h > #include <stdio.h > int main() { struct stat t; printf( "sizeof = %zu, offset = %td\n ", sizeof(t), ((void *) &t.st_size) - ((void *) &t)); return 0; }
На моей системе программа напечатала sizeof = 88, offset = 44. На вашей системе это значение может отличаться по описанным причинам. Теперь у нас есть все нужные данные об этой структуре, пишем программу:
.data str_usage: .string "usage: %s filename\n " printf_format: .string "%u\n " .text .globl main main: pushl %ebp movl %esp, %ebp subl $88, %esp /* выделить 88 байт под struct stat */ cmpl $2, 8(%ebp) /* argc == 2? */ je args_ok /* программе передали не 2 аргумента, вывести usage */ movl 12(%ebp), %ebx /* поместить в %ebx адрес массива argv */ pushl (%ebx) /* argv[0] */ pushl $str_usage call printf movl $1, %eax /* выйти с кодом 1 */ jmp return args_ok: leal -88(%ebp), %ebx /* поместить адрес структуры в регистр %ebx */ pushl %ebx movl 12(%ebp), %ecx /* поместить в %ecx адрес массива argv */ pushl 4(%ecx) /* argv[1] - имя файла */ call stat cmpl $0, %eax /* stat() вернул 0? */ je stat_ok /* stat() вернул ошибку, нужно вызвать perror(argv[1]) и завершить программу */ movl 12(%ebp), %ecx pushl 4(%ecx) call perror movl $1, %eax jmp return stat_ok: pushl 44(%ebx) /* нужное нам поле по смещению 44 */ pushl $printf_format call printf movl $0, %eax /* выйти с кодом 0 */ return: movl %ebp, %esp popl %ebp ret
Обратите внимание на обработку ошибок: если передано не 2 аргумента - выводим описание использования программы и выходим, если stat(2) вернул ошибку - выводим сообщение об ошибке и выходим.
Наверное, могут возникнуть некоторые сложности с пониманием, как расположены argc и argv в стеке. Допустим, вы запустили программу как
[user@host:~]$ ./program test-file
Тогда стек будет выглядеть приблизительно так:
. . . . . . +------------------------+ 0x0000EFE4 <-- %ebp - 88 | struct stat | +------------------------+ 0x0000F040 <-- %ebp | старое значение %ebp | +------------------------+ 0x0000F044 <-- %ebp + 4 | адрес возврата | +------------------------+ 0x0000F048 <-- %ebp + 8 | argc | +------------------------+ 0x0000F04C <-- %ebp + 12 | указатель на argv[0] | ---------------------------- +------------------------+ 0x0000F050 <-- %ebp + 16 | . . --------------- . . | . . V +-------------+ +-------------+ | argv[0] | ----- > | "./program " | +-------------+ +-------------+ | argv[1] | -\ +-------------+ \ +-------------+ | argv[2] = 0 | \- > | "test-file " | +-------------+ +-------------+
Таким образом, в стек помещается два параметра: argc и указатель на первый элемент массива argv[]. Где-то в памяти расположен блок из трёх указателей: указатель на строку "./program ", указатель на строку "test-file " и указатель NULL. Нам в стеке передали адрес этого блока памяти.
Программа: печать файла наоборот
Напишем программу, которая читает со стандартного ввода всё до конца файла, а потом выводит введённые строки в обратном порядке. Для этого мы во время чтения будем помещать строки в связный список, а потом пройдем этот список в обратном порядке и напечатаем строки.
.data printf_format: .string " <%s >\n " #define READ_CHUNK 128 .text /* char *read_str(int *is_eof) */ read_str: pushl %ebp movl %esp, %ebp pushl %ebx /* сохранить регистры */ pushl %esi pushl %edi movl $0, %ebx /* прочитано байт */ movl $READ_CHUNK, %edi /* размер буфера */ pushl %edi call malloc addl $4, %esp /* убрать аргументы */ movl %eax, %esi /* указатель на начало буфера */ decl %edi /* в конце должен быть нулевой байт, зарезервировать место для него */ pushl stdin /* fgetc() всегда будет вызываться с этим аргументом */ 1: /* read_start */ call fgetc /* прочитать 1 символ */ cmpl $0xa, %eax /* новая строка '\n '? */ je 2f /* read_end */ cmpl $-1, %eax /* конец файла? */ je 4f /* eof_yes */ movb %al, (%esi,%ebx,1) /* записать прочитанный символ в буфер */ incl %ebx /* инкрементировать счётчик прочитанных байт */ cmpl %edi, %ebx /* буфер заполнен? */ jne 1b /* read_start */ addl $READ_CHUNK, %edi /* увеличить размер буфера */ pushl %edi /* размер */ pushl %esi /* указатель на буфер */ call realloc addl $8, %esp /* убрать аргументы */ movl %eax, %esi /* результат в %eax - новый указатель */ jmp 1b /* read_start */ 2: /* read_end */ 3: /* eof_no */ movl 8(%ebp), %eax /* *is_eof = 0 */ movl $0, (%eax) jmp 5f /* eof_end */ 4: /* eof_yes */ movl 8(%ebp), %eax /* *is_eof = 1 */ movl $1, (%eax) 5: /* eof_end */ movb $0, (%esi,%ebx,1) /* записать в конец буфера '\0 ' */ movl %esi, %eax /* результат в %eax */ addl $4, %esp /* убрать аргумент fgetc() */ popl %edi /* восстановить регистры */ popl %esi popl %ebx movl %ebp, %esp popl %ebp ret /* struct list_node { struct list_node *prev; char *str; }; */ .globl main main: pushl %ebp movl %esp, %ebp subl $4, %esp /* int is_eof; */ movl $0, %edi /* в %edi будет храниться указатель на предыдущую структуру */ 1: /* read_start */ leal -4(%ebp), %eax /* %eax = &is_eof; */ pushl %eax call read_str movl %eax, %esi /* указатель на прочитанную строку поместить в %esi */ pushl $8 /* выделить 8 байт под структуру */ call malloc movl %edi, (%eax) /* указатель на предыдущую структуру */ movl %esi, 4(%eax) /* указатель на строку */ movl %eax, %edi /* теперь эта структура - предыдущая */ addl $8, %esp /* убрать аргументы */ cmpl $0, -4(%ebp) /* is_eof == 0? */ jne 2f jmp 1b 2: /* read_end */ 3: /* print_start */ /* просматривать список в обратном порядке, так что в %edi адрес текущей структуры */ pushl 4(%edi) /* указатель на строку из текущей структуры */ pushl $printf_format call printf /* вывести на экран */ addl $4, %esp /* убрать из стека только $printf_format */ call free /* освободить память, занимаемую строкой */ pushl %edi /* указатель на структуру для освобождения памяти */ movl (%edi), %edi /* заменить указатель в %edi на следующий */ call free /* освободить память, занимаемую структурой */ addl $8, %esp /* убрать аргументы */ cmpl $0, %edi /* адрес новой структуры == NULL? */ je 4f jmp 3b 4: /* print_end */ movl $0, %eax /* выйти с кодом 0 */ return: movl %ebp, %esp popl %ebp ret
[user@host:~]$ gcc print.S -o print [user@host:~]$ ./print aaa bbbb ccccc ^D < > <ccccc > <bbbb > <aaa >
Обратите внимание, что мы ввели 4 строки: "aaa ", "bbbb ", "ccccc ", " ".
В этой программе был использован некоторый новый синтаксис. Во-первых, вы видите директиву препроцессора #define. Препроцессор Си (cpp) может быть использован для обработки исходного кода на ассемблере: нужно всего лишь использовать расширение .S для файла с исходным кодом. Файлы с таким расширением gcc предварительно обрабатывает препроцессором cpp, после чего компилирует как обычно.
Во-вторых, были использованы метки-числа, причём некоторые из них повторяются в двух функциях. Почему бы не использовать текстовые метки, как в предыдущих примерах? Можно, но они должны быть уникальными. Например, если бы мы определили метку read_start и в функции read_str, и в main, GCC бы выдал ошибку при компиляции:
[user@host:~]$ gcc print.S print.S: Assembler messages: print.S:85: Error: symbol `read_start ' is already defined
Поэтому, используя текстовые метки, приходится каждый раз придумывать уникальное имя. А можно использовать метки-числа, компилятор преобразует их в уникальные имена сам. Чтобы поставить метку, просто используйте любое положительное число в качестве имени. Чтобы сослаться на метку, которая определена ранее, используйте Nb (мнемоническое значение - backward), а чтобы сослаться на метку, которая определена дальше в коде, используйте Nf (мнемоническое значение - forward).
Операции с цепочками данных
При обработке данных часто приходится иметь дело с цепочками данных. Цепочка, как подсказывает название, представляет собой массив данных - несколько переменных одного размера, расположенных друг за другом в памяти. В Си вы использовали массив и индексную переменную, например, argv[i]. Но в ассемблере для последовательной обработки цепочек есть специализированные команды. Синтаксис:
lods stos
"Странно ", - скажет кто-то, - "откуда эти команды знают, где брать данные и куда их записывать? Ведь у них и аргументов-то нет! " Вспомните про регистры %esi и %edi и про их немного странные имена: "индекс источника " (англ. source index) и "индекс приёмника " (англ. destination index). Так вот, все цепочечные команды подразумевают, что в регистре %esiнаходится указатель на следующий необработанный элемент цепочки-источника, а в регистре %edi - указатель на следующий элемент цепочки-приёмника.
Направление просмотра цепочки задаётся флагом df: 0 - просмотр вперед, 1 - просмотр назад.
Итак, команда lods загружает элемент из цепочки-источника в регистр %eax/%ax/%al (размер регистра выбирается в зависимости от суффикса команды). После этого значение регистра %esi увеличивается или уменьшается (в зависимости от направления просмотра) на значение, равное размеру элемента цепочки.
Команда stos записывает содержимое регистра %eax/%ax/%al в цепочку-приёмник. После этого значение регистра %edi увеличивается или уменьшается (в зависимости от направления просмотра) на значение, равное размеру элемента цепочки.
Вот пример программы, которая работает с цепочечными командами. Конечно же, она занимается бестолковым делом, но в противном случае она была бы гораздо сложнее. Она увеличивает каждый байт строки str_in на 1, то есть заменяет a на b, b на с, и так далее.
.data printf_format: .string "%s\n " str_in: .string "abc123()!@!777 " .set str_in_length, .-str_in .bss str_out: .space str_in_length .text .globl main main: pushl %ebp movl %esp, %ebp movl $str_in, %esi /* цепочка-источник */ movl $str_out, %edi /* цепочка-приёмник */ movl $str_in_length - 1, %ecx /* длина строки без нулевого байта (нулевой байт не обрабатываем) */ 1: lodsb /* загрузить байт из источника в %al */ incb %al /* произвести какую-то операцию с %al */ stosb /* сохранить %al в приёмнике */ loop 1b movsb /* копировать нулевой байт */ /* важно: сейчас %edi указывает на конец цепочки-приёмника */ pushl $str_out pushl $printf_format call printf /* вывести на печать */ movl $0, %eax movl %ebp, %esp popl %ebp ret [user@host:~]$ ./stringop bcd234)* "A "888 [user@host:~]$
Но с цепочками мы часто выполняем довольно стандартные действия. Например, при копировании блоков памяти мы просто пересылаем байты из одной цепочки в другую, без обработки. При сравнении строк мы сравниваем элементы двух цепочек. При вычислении длины строки в Си мы считаем байты до тех пор, пока не встретим нулевой байт. Эти действия очень просты, но, в тоже время, используются очень часто, поэтому были введены следующие команды:
movs cmps scas