| Россия |
Команды ассемблера
Значит, 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

