Опубликован: 01.03.2016 | Доступ: свободный | Студентов: 586 / 62 | Длительность: 03:55:00
Лекция 5:

Команды ассемблера

Значит, 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. Нам в стеке передали адрес этого блока памяти.

Программа: печать файла наоборот

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


Внимание! Сохраните исходный код этой программы в файл с расширением .S - S в верхнем регистре.
.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

Для того, чтобы послать с клавиатуры сигнал о конце файла, нажмите Ctrl-D
 
[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