Copyright (C) А.Гавва V-0.4w май 2004

9. Использование встроенного ассемблера

Использование встроенного ассемблера вместе с компилятором GNAT в реальности не является сложностью и позволяет генерировать очень эффективный код. Однако, для того кто использовал средства встроенного ассемблера с другими языками программирования и компиляторами, использование встроенного ассемблера вместе с компилятором GNAT может сначала показаться несколько мистическим.

Прежде чем продолжить обсуждение этой темы, необходимо сделать несколько замечаний:

9.1 Общие сведения

9.1.1 Пакет System.Machine_Code

Ада-программист не часто нуждается в непосредственном использовании ассемблера. Однако, когда возникает ситуация при которой необходимо использовать ассемблер, тогда действительно необходимо использовать ассемблер. Для таких случаев Ада предоставляет пакет System.Machine_Code который позволяет использовать ассемблер внутри Ада-программы. Поскольку разработчики стандарта Ады не имели возможности предусмотреть каких-либо разумных средств, которые обязан предоставлять этот пакет, то ответственность за реальную реализацию этих средств полностью возложена на разработчиков конкретного компилятора, которые способны адаптировать свой компилятор под то окружение в котором компилятор будет использоваться. Следовательно, здесь основное внимание сосредоточено на реализации этого пакета для компилятора GNAT.

9.1.2 Различия в использовании внешнего и встроенного ассемблера

Справедливо заметить, что всегда существует возможность скомпоновать программу на языке Ада с внешней подпрограммой на ассемблере. Однако, для небольших фрагментов ассемблерного кода, использование встроенного ассемблера обладает несколькими отличительными преимуществами:

Таким образом, в случаях когда нет необходимости писать большое количество ассемблерного кода, удобнее использовать средства встроенного ассемблера.

9.1.3 Особенности реализации компилятора GNAT

Поскольку компилятор GNAT является частью семейства компиляторов GCC, то он использует средства встроенного ассемблера, которые характерны для этого семейства компиляторов. В следствие этого, реализация GNAT обладает следующими основными преимуществами:

В результате, возможность отображения естественных средств GCC на Ada95 выглядит достаточно впечатляющей.

9.2 Особенности используемого ассемблера

Для тех кто мигрирует с платформы PC/MS, вид ассемблера, используемого в GNAT, может сначала показаться обескураживающим. Причина в том, что ассемблер, который используется семейством компиляторов GCC, не использует привычный язык ассемблера Intel, а использует язык ассемблера, который происходит от ассемблера as системы AT&T Unix (его часто называют ассемблером с синтаксисом AT&T). Таким образом, даже человек, который знаком с языком ассемблера Intel, будет вынужден потратить некоторое время на изучение нового языка ассемблера, перед тем как использовать ассемблер с GNAT.

9.2.1 Именование регистров процессора

При использовании соглашений Intel, именование регистров процессора осуществляется следующим образом:

    
    
    mov eax, 4
    

В случае синтаксиса AT&T, перед именем регистра процессора должен располагаться символ процента '%':

    
    
    mov %eax, 4
    

9.2.2 Порядок следования операндов источника и приемника

В действительности, приведенный выше пример не будет успешно ассемблироваться с GNAT. В то время как соглашения Intel устанавливают порядок следования операндов источника и приемника как:

    
    
    mov приемник, источник
    

синтаксис AT&T использует обратный порядок:

    
    
    mov источник, приемник
    

Таким образом пример должен быть переписан:

    
    
    mov 4, %eax
    

9.2.3 Значения констант

Это может показаться странным, но попытка ассемблирования показанного ранее кода также окажется безуспешной. Причина в том, что синтаксис AT&T подразумевает, что перед непосредственными статическими константными значениями необходимо помещать символ доллара '$':

    
    
    mov $4, %eax
    

В результате, этот код выполнит загрузку регистра eax значением 4. Такая же нотация используется когда необходимо загрузить в регистр какое-либо адресное значение. Например, можно написать следующее:

    
    
    mov $my_var, %eax
    

для загрузки адреса переменной my_var в регистр eax.

9.2.4 Шестнадцатеричные значения

При использовании соглашений Intel, для загрузки в регистр шестнадцатеричного значения можно было написать:

    
    
    mov eax, 1EAh
    

Увы, в этом случае синтаксис AT&T также отличается! Для загрузки в регистр шестнадцатеричного значения необходимо использовать соглашения языка C, то есть, шестнадцатеричное значение должно иметь префикс 0x, причем, даже при использовании встроенного ассемблера Ады. Таким образом, показанный выше пример должен быть переписан следующим образом:

    
    
    mov $0x1EA, %eax
    

где сама константа не зависит от используемого регистра символов.

9.2.5 Суффиксы размера

Ассемблер Intel пытается определить размер инструкции пересылки анализируя размеры операндов. Таким образом, код:

    
    
    mov ax, 10
    

будет пересылать 16-битное слово в регистр ax. Ассемблер as, как правило, отказывается "играть в догадки". Вместо этого, при написании имени инструкции необходимо использовать суффикс явной символьной индикации размеров операндов:

    
    
    movw $10, %ax
    

где допустимыми символами являются b, w и l:

Можно встретить и другие случаи использования модификаторов размера, подобно тому как pushfd становиться pushfl. Чаще всего использование модификаторов размера очевидно, а в случае ошибки - ассемблер обязательно подскажет.

9.2.6 Загрузка содержимого памяти

Ранее мы рассмотрели инструкцию вида:

    
    
    movl $my_var, %eax
    

которая позволяет загрузить регистр eax значением адреса переменной my_var. А как можно загрузить в регистр eax значение содержимого переменной my_var? Используя привычный синтаксис Intel можно написать:

    
    
    mov eax, [my_var]
    

Синтаксис AT&T потребует изменить это на:

    
    
    movl my_var, %eax
    

9.2.7 Косвенная адресация

Теперь нам известно как загрузить значение адреса и содержимое памяти. Но как загрузить содержимое памяти на которое указывает адрес расположенный в памяти или регистре? Синтаксис AT&T не использует средства подобные "BYTE PTR", которые традиционны для ассемблера Intel. Для загрузки регистра ebx 32-битным содержимым памяти адрес которого храниться в регистре eax необходимо написать:

    
    
    movl (%eax), %ebx
    

Для добавления смещения к адресу в регистре eax, необходимо использовать это смещение как префикс. Например, для получения содержимого 4-мя байтами ниже адреса, указанного в регистре eax, следует написать:

    
    
    movl -4(%eax), %ebx
    

или использовать содержимое памяти подобным образом:

    
    
    movl my_var(%eax), %ebx
    

Существует большое количество возможных схем адресации (при необходимости, лучше обратиться к руководству по as) однако того, что перечислено, как правило, достаточно для использования встроенного ассемблера.

9.2.8 Инструкции повторения

При использовании синтаксиса Intel, инструкции повторения записывались в одной строке кода. Так, для пересылки строки, можно было написать:

    
    
    rep stos
    

В случае синтаксиса AT&T, эти инструкции должны быть записаны в отдельных строчках ассемблерного кода:

    
    
    rep
    stosl
    

9.3 Использование пакета System.Machine_Code

После рассмотрения общих правил написания инструкций ассемблера, можно сконцентрировать внимание на интеграции кода ассемблера с Ада-программой GNAT.

9.3.1 Пример элементарной программы

Пожалуй, более элементарную программу придумать не возможно:

    
    
    with System.Machine_Code; use System.Machine_Code;
    
    procedure Nothing is
    begin
        Asm ("nop");
    end Nothing;
    

Не составит труда догадаться, что эта программа состоит из единственной инструкции ассемблера nop, которая действительно ничего не делает.

9.3.2 Проверка примера элементарной программы

Для исследования показанного примера элементарной программы, который демонстрирует использование встроенного ассемблера, можно воспользоваться способностью GNAT генерировать ассемблерный листинг программы. Это позволяет убедиться в том, что мы реально получаем то, что и ожидаем получить. При последующем рассмотрении более сложных примеров использования встроенного ассемблера и Ады, а также при самостоятельном использовании встроенного ассемблера такая способность GNAT будет оценена по достоинству. Следует также заметить, что анализ генерируемого ассемблерного листинга может оказаться более еффективным способом отладки программы чем компиляция и запуск отладчика. Для того, чтобы получить ассемблерный листинг программы необходимо выполнить следующую команду:

    
    
       gcc -c -S -fomit-frame-pointer -gnatp 'nothing.adb'
    

Поясним значение используемых в этой команде опций:

Выполнение команды предоставляет хорошо читаемую версию ассемблерного кода. Результирующий файл будет иметь такое же базовое имя, а расширение имени файла будет .s. В случае файла с исходным текстом nothing.adb, результирующий файл с ассемблерным листингом будет nothing.s. Содержимое этого файла будет иметь следующий вид:

    
    
    .file "nothing.adb"
    gcc2_compiled.:
    ___gnu_compiled_ada:
    .text
       .align 4
    .globl __ada_nothing
    __ada_nothing:
    #APP
       nop
    #NO_APP
       jmp L1
       .align 2,0x90
    L1:
       ret
    

Примечательно, что ассемблерный код, который непосредственно был введен в исходный текст примера программы, помещен между метками #APP и #NO_APP.

9.3.3 Поиск ошибок в коде ассемблера

Часто, при совершении ошибки в ассемблерном коде (подобной использованию не правильного модификатора размера или оператора для какой-либо инструкции) GNAT сообщает о такой ошибке во временном файле, который уничтожается после завершения процесса компиляции. В подобных ситуациях, генерация ассемблерного файла, показанная в примере выше, может оказаться не заменимым отладочным средством, поскольку полученный ассемблерный файл может быть самостоятельно ассемблирован с помощью ассемблера as (используемый в системе или поставляемый вместе с GNAT). Так, для ассемблирования файла nothing.s, полученного ранее с помощью GNAT, можно использовать следующую команду:

    
    
    as nothing.s
    

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

9.3.4 Более реальный пример

Более реальным и интересным примером может служить пример, который позволет увидеть содержимое регистра флагов процессора:

    
    
    with Interfaces;             use Interfaces;
    with Ada.Text_IO;            use Ada.Text_IO;
    with System.Machine_Code;    use System.Machine_Code;
    with Ada.Characters.Latin_1; use Ada.Characters.Latin_1;
    
    procedure Getflags is
       Eax : Unsigned_32;
    begin
       Asm ("pushfl"          & LF & HT & -- сохранить регистр флагов в стеке
            "popl %%eax"      & LF & HT & -- загрузить флаги из стека в регистр eax
            "movl %%eax, %0",             -- сохранить значение флагов в переменной
            Outputs => Unsigned_32'Asm_Output ("=g", Eax));
       Put_Line ("Flags register:" & Eax'Img);
    end Getflags;
    

Текст этого примера может неожиданно показаться несколько сложным. Однако это не должно вызывать большие опасения, поскольку далее будут рассмотрены все тонкости этого примера. Следует заметить, что понимание текста этого примера позволит использовать всю мощь встроенного ассемблера GNAT.

Первое, на что необходимо обратить внимание - это способ записи множества инструкций ассемблера. Реально, следующий пример записи множества инструкций ассемблера будет полностью корректным:

    
    
    Asm ("pushfl popl %%eax movl %%eax, %0")
    

В генерируемом ассемблерном файле этот фрагмент будет отображен следующим образом:

    
    
    #APP
       pushfl popl %eax movl %eax, -40(%ebp)
    #NO_APP
    

Не трудно заметить, что такой результат не очень легко и удобно читать.

Таким образом, удобнее записывать различные инструкции ассемблера в отдельных строчках исходного текста, заключая их в двойные кавычки, а с помощью стандартного пакета Ada.Characters.Latin_1 выполнять "ручную" вставку символов перевода строки(LineFeed / LF) и горизонтальной табуляции (Horizontal Tab / HT). Это позволяет сделать более читабельным как исходный текст Ады:

    
    
    Asm ("pushfl"          & LF & HT & -- сохранить регистр флагов в стеке
         "popl %%eax"      & LF & HT & -- загрузить флаги из стека в регистр eax
         "movl %%eax, %0")             -- сохранить значение флагов в переменной
    

так и генерируемый файл ассемблера:

    
    
    #APP
       pushfl
       popl %eax
       movl %eax, -40(%ebp)
    #NO_APP
    

Однако, стоит обратить внимание на тот факт, что Ада полностью игнорирует комментарии в конце строки при записи реальных инструкций ассемблера в генерируемый ассемблерный файл.

Настоятельно рекомендуется применять данный способ для оформления исходных текстов с использованием встроенного ассемблера, поскольку далеко не все (включая и автора, спустя несколько месяцев) будут способны легко понять смысл фрагмента исходного текста на ассемблере!

При более внимательном рассмотрении исходного текста примера можно обнаружить примечательный факт наличия двух символов процента перед каждым именем регистра, хотя в генерируемом ассемблерном файле перед именами регистров присутствует только один символ процента. Причина этого заключается в том, что при написании инструкций встроенного ассемблера, всем именам переменных и именам регистров должен предшествовать символ процента '%'. Следовательно, поскольку ассемблер сам требует использование символа процента перед именем регистра, то мы получаем следующую инструкцию встроенного ассемблера для пересылки содержимого из регистра ax в регистр bx:

    
    
    Asm ("movw %%ax, %%bx");
    

Это будет отображено в сгенерированном ассемблерном файле как:

    
    
    #APP
       movw %ax, %bx
    #NO_APP
    

Фактическое использование символа процента в инструкциях ассемблера необходимо для введения операнда более общего вида:

    
    
    Asm ("pushfl"          & LF & HT & -- сохранить регистр флагов в стеке
         "popl %%eax"      & LF & HT & -- загрузить флаги из стека в регистр eax
         "movl %%eax, %0",             -- сохранить значение флагов в переменной
         Outputs => Unsigned_32'Asm_Output ("=g", Eax));
    

Здесь, %0, %1, %2... индицируют операнды которые позже описывают использование параметров ввода (Input) и вывода (Output).

9.3.5 Параметры вывода

При желании сохранить содержиме регистра eax в какой-либо переменной, первым желанием будет написать что-нибудь подобное следующему:

    
    
    procedure Getflags is
       Eax : Unsigned_32;
    begin
       Asm ("pushfl"           & LF & HT & -- сохранить регистр флагов в стеке
            "popl %%eax"       & LF & HT & -- загрузить флаги из стека в регистр eax
            "movl %%eax, Eax")             -- сохранить значение флагов в переменной
       Put_Line ("Flags register:" & Eax'Img);
    end Getflags;
    

с попыткой сразу сохранить содержимое регистра процессора eax в переменной Eax. Увы, также просто как это может выглядеть, это не будет работать, поскольку нельзя точно сказать чем будет являться переменная Eax. Несколько подумав, можно сказать: Eax - это локальная переменная, которая, в обычной ситуации, будет размещена в стеке. Однако, в результате оптимизации, компилятор, для хранения этой переменной во время выполнения подпрограммы, может использовать не пространство в стеке, а обычный регистр процессора. Таким образом, возникает законный вопрос: как необходимо специфицировать переменную, чтобы обеспечить корректную работу для обоих случаев? Ответ на этот вопрос заключается в том, чтобы не сохранять результат в переменной Eax самостоятельно, а предоставить компилятору возможность самостоятельно выбирать правильный операнд для использования. Для этой цели применяется понятие параметра вывода.

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

    
    
    Asm ("pushfl"          & LF & HT & -- сохранить регистр флагов в стеке
         "popl %%eax"      & LF & HT & -- загрузить флаги из стека в регистр eax
         "movl %%eax, %0")             -- сохранить значение флагов в переменной
    

Следует обратить внимание на то, что мы заменили обращение к переменной Eax на обращение к операнду %0. Однако, компилятору необходимо указать, чем является %0:

    
    
    Outputs => Unsigned_32'Asm_Output ("=g", Eax));
    

Здесь, часть "Outputs =>" укажет, что это именованный параметр инструкции ассемблера (полный синтаксис инструкций ассемблера будет рассмотрен позже в "Синтаксис GNAT"). Напомним также, что ранее мы описали переменную Eax, как переменную типа Interfaces.Unsigned_32. Мы описали ее таким образом, поскольку 32-битный беззнаковый целый тип удачнее всего подходит для представления регистра процессора. Не трудно заметить, что вывод назначен как атрибут типа, который мы реально хотим использовать для нашего вывода.

    
    
    Unsigned_32'Asm_Output ("=g", Eax);
    

Общая форма такого описания имеет следующий вид:

    
    
    Type'Asm_Output (строка_ограничений, имя_переменной)
    

Смысл имени переменной и атрибута 'Asm_Output достаточно понятны. Остается понять, что означает и для чего используется строка_ограничений.

9.3.6 Ограничения

Строка ограничений, состоящая из символов, указывает компилятору на то, как он должен управлять переменной, которую мы ему предоставляем. Это значит, что мы указываем компилятору то, как мы интерпретируем смысл этой переменной. Например:

    
    
    Unsigned_32'Asm_Output ("=m", Eax);
    

Здесь, использование ограничения m (memory) указывает компилятору, что переменная Eax должна быть переменной которая размещается в памяти. В результате указания этого ограничения, компилятор никогда не будет использовать регистр процессора для хранения этой переменной.

Рассмотрим еще один пример указания ограничения:

    
    
    Unsigned_32'Asm_Output ("=r", Eax);
    

Здесь, использование ограничения r (register) указывает компилятору на использование регистровой переменной. Следовательно, для хранения этой переменной компилятор будет использовать регистр процессора. Если ограничению предшествует символ равенства ("="), то это указывает компилятору, что переменная будет использоваться для сохранения данных.

В показанном ранее примере, при указании ограничения, использовалось ограничение g (global), что позволяет оптимизатору использовать то, что он сочтет более эффективным.

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

При использовании другого типа процессора или при необходимости получения более исчерпывающей информации об ограничениях, следует обратиться к руководствам по gcc и as. При необходимости использования более специфических ограничений, которые описаны в руководстве по gcc, следует помнить, что допускается использование комбинаций из нескольких самостоятельных ограничений.

В настоящий момент должно быть понятно, что ограничения используются для указания компилятору как необходимо управлять конкретной переменной (или как ее интерпретировать). Теперь необходимо рассмотреть то, как указать компилятору где эта переменная находится. Для связывания параметра вывода с ассемблерным операндом используется %x-нотация, в которой x является числом.

    
    
    Asm ("pushfl"          & LF & HT & -- сохранить регистр флагов в стеке
         "popl %%eax"      & LF & HT & -- загрузить флаги из стека в регистр eax
         "movl %%eax, %0",             -- сохранить значение флагов в переменной
         Outputs => Unsigned_32'Asm_Output ("=g", Eax));
    

Таким образом, в показанном выше фрагменте кода, %0 будет заменяться фактическим кодом, в соответствии с решением компилятора о фактическом месторасположении переменной Eax. Означает-ли это, что мы можем иметь только одну переменную вывода? Нет, мы можем иметь их столько сколько нам необходимо. Это работает достаточно просто:

Для демонстрации сказанного, приведем простой пример:

    
    
    Asm ("movl %%eax, %0" &
         "movl %%ebx, %1" &
         "movl %%ecx, %2",
         Outputs => (Unsigned_32'Asm_Output ("=g", Eax"), --  %0 = Eax
                    (Unsigned_32'Asm_Output ("=g", Ebx"), --  %1 = Ebx
                    (Unsigned_32'Asm_Output ("=g", Ecx)); --  %2 = Ecx
    

Следует нпомнить с чего начиналось написание нашего примера:

    
    
    Asm ("pushfl"          & LF & HT & -- сохранить регистр флагов в стеке
         "popl %%eax"      & LF & HT & -- загрузить флаги из стека в регистр eax
         "movl %%eax, %0",             -- сохранить значение флагов в переменной
    Outputs => Unsigned_32'Asm_Output ("=g", Eax));
    

Фактически, мы можем использовать регистровое ограничение для указания компилятору на необходимость сохранять содержимое регистра eax в переменной Eax путем написания следующих инструкций встроенного ассемблера:

    
    
    with Interfaces;             use Interfaces;
    with Ada.Text_IO;            use Ada.Text_IO;
    with System.Machine_Code;    use System.Machine_Code;
    with Ada.Characters.Latin_1; use Ada.Characters.Latin_1;
    
    procedure Flags is
       Eax : Unsigned_32;
    begin
       Asm ("pushfl"      & LF & HT & -- сохранить регистр флагов в стеке
            "popl %%eax",             -- загрузить флаги из стека в регистр eax
            Outputs => Unsigned_32'Asm_Output ("=a", Eax));
       Put_Line ("Flags register:" & Eax'Img);
    end Flags;
    

Примечательно, что ограничение a, указывает компилятору, что переменная Eax будет располагаться в регистре eax. Генерируемый компилятором, код ассемблера будет иметь следующий вид:

    
    
    #APP
       pushfl
       popl %eax
    #NO_APP
       movl %eax,-40(%ebp)
    

Очевидно, что значение eax сохранено в Eax компилятором после выполнения кода ассемблера.

Следя за последовательностью обсуждения, внимательный читатель может быть удивлен необходимостью сохранения содержимого регистра eax в переменной Eax, когда значение регистра флагов можно сразу вытолкнуть (pop) в переменную вывода. Следует согласиться с тем, что такое удивление справедливо. Это было необходимо как стартовая точка дискуссии посвященной ограничениям, но, в действительности, в этом нет необходимости. Таким образом, мы можем переписать исходный текст следующим образом:

    
    
    with Interfaces;             use Interfaces;
    with Ada.Text_IO;            use Ada.Text_IO;
    with System.Machine_Code;    use System.Machine_Code;
    with Ada.Characters.Latin_1; use Ada.Characters.Latin_1;
    
    procedure Okflags is
       Eax : Unsigned_32;
    begin
       Asm ("pushfl"  & LF & HT & -- сохранить регистр флагов в стеке
            "pop %0",             -- загрузить флаги из стека в переменную Eax
            Outputs => Unsigned_32'Asm_Output ("=g", Eax));
       Put_Line ("Flags register:" & Eax'Img);
    end Okflags;
    

В результате, мы получим результат, который был показан ранее.

9.3.7 Использование самостоятельно описываемых типов

Параметры вывода позволяют использовать не только предопределенные типы. Они также позволяют использовать типы которые самостоятельно описаны программистом. В качестве примера, рассмотрим ситуацию в которой необходимо прочитать содержимое регистра флагов процессора и проверить состояние флага переноса (carry flag). Известно, что флаг переноса находится в бите 0 регистра флагов процессора. Следовательно, можно использовать соответствующую битовую маску для проверки его состояния:

    
    
    if (Eax and 16#00000001#) = 1 then
       Put_Line ("Carry flag set");
    end if;
    

Однако, может возникнуть необходимость в установке или чтении других битов. В таком случае, для облегчения доступа к индивидуальным битам, может оказаться более предпочтительным применение соответствующего типа данных. Примером, который демонстрирует такой подход, может служить следующая программа:

    
    
    with Ada.Text_IO;            use Ada.Text_IO;
    with System.Machine_Code;    use System.Machine_Code;
    with Ada.Characters.Latin_1; use Ada.Characters.Latin_1;
    
    procedure Bits is
    
       subtype Num_Bits is Natural range 0 .. 31;
    
       type Flags_Register is array (Num_Bits) of Boolean;
       pragma Pack (Flags_Register);
       for Flags_Register'Size use 32;
    
       Carry_Flag : constant Num_Bits := 0;
    
       Register : Flags_Register;
    
    begin
    
       Asm ("pushfl" & LF & HT & --  сохранить регистр флагов в стеке
            "pop %0",            --  загрузить флаги из стека в переменную Register
            Outputs => Flags_Register'Asm_Output ("=g", Register));
    
       if Register(Carry_Flag) = True then
          Put_Line ("Carry flag set");
       end if;
    
    end Bits;
    

Следует заметить, что в подобных случаях выбор варианта решения больше зависит от требований, которые предъявляются к уровню абстракции в программе. При использовании полной оптимизации (-O2), генерируемый компилятором код ассемблера будет идентичен для обоих вариантов:

    
    
    #APP
       pushfl
       pop %eax
    #NO_APP
       testb $1,%al
    

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

9.3.8 Параметры ввода

До сих пор мы рассматривали программы на встроенном ассемблере, которые способны осуществлять только вывод информации. Однако также часто возникает необходимость в осуществлении ввода информации в программу которая написана на встроенном ассемблере. Рассмотрим пример программы, которая использует функцию написанную на встроенном ассемблере, причем, эта функция принимает входное значение своего параметра, увеличивает это значение на единицу, а затем возвращает его в качестве результата.

    
    
    with Interfaces;           use Interfaces;
    with Ada.Text_IO;          use Ada.Text_IO;
    with System.Machine_Code;  use System.Machine_Code;
    
    procedure Inc_It is
    
       function Increment (Value : Unsigned_32) return Unsigned_32 is
          Result : Unsigned_32;
       begin
          Asm ("incl %0",
               Inputs  => Unsigned_32'Asm_Input ("a", Value),
               Outputs => Unsigned_32'Asm_Output ("=a", Result));
          return Result;
       end Increment;
    
       Value : Unsigned_32;
    
    begin
       Value := 5;
       Put_Line ("Value before is" & Value'Img);
       Value := Increment (Value);
       Put_Line ("Value after is" & Value'Img);
    end Inc_It;
    

Следует заметить, что на данном этапе принцип кодирования инструкций встроенного ассемблера уже должен быть понятен:

    
    
    Asm ("incl %0",
         Inputs  => Unsigned_32'Asm_Input ("a", Value),
         Outputs => Unsigned_32'Asm_Output ("=a", Result));
    

Как видно из этого примера, параметр вывода описан так же как и ранее, определяя, что результат, полученный в регистре eax, будет сохранен в переменной Result. Описание параметра ввода во многом похоже на описание параметра вывода, но использует атрибут 'Asm_Input вместо атрибута 'Asm_Output. Кроме того, при описании параметра ввода, отсутствует указание ограничения =, указывающего на вывод значения. Также как и в случае параметров вывода, допускается наличие множества параметров ввода. Отсчет параметров (%0, %1, ...) начинается с первого параметра ввода и продолжается при перечислении инструкций вывода. Следует заметить, что в специальных случаях, когда оба параметра используют один и тот же регистр процессора, компилятор будет рассматривать их как один и тот же %x операнд (например, как в этом случае). Не трудно догадаться, что если параметр вывода сохраняет значение регистра в переменной назначения после выполнения инструкций ассемблера, то параметр ввода используется для загрузки значения переменной в регистр процессора до начала выполнения инструкций ассемблера. Таким образом, следующий фрагмент кода:

    
    
    Asm ("incl %0",
         Inputs  => Unsigned_32'Asm_Input ("a", Value),
         Outputs => Unsigned_32'Asm_Output ("=a", Result));
    

указывает компилятору:

  1. загрузить 32-битное значение переменной Value в регистр eax
  2. выполнить инструкцию incl %eax
  3. сохранить содержимое регистра eax в переменной Result

Если посмотреть на сгенерированный компилятором файл ассемблера (используя полную оптимизацию), то можно обнаружить следующее:

    
    
    _inc_it__increment.1:
       subl $4,%esp
       movl 8(%esp),%eax
    #APP
       incl %eax
    #NO_APP
       movl %eax,%edx
       movl %ecx,(%esp)
       addl $4,%esp
       ret
    

При внимательном рассмотрении всего ассемблерного файла, который был сгенерирован компилятором, можно заметить, что функция, которая описана внутри процедуры, содержит много дополнительного кода, необходимого для того, чтобу установить для этой функции отдельный кадр стека. Следовательно, чтобы улучшить производительность, необходимо описывать маленькие функции на уровне библиотеки или оформлять их как функции для встроенной подстановки (inline).

9.3.9 Встроенная подстановка (inline) для кода на встроенном ассемблере

При использовании маленьких функций, затраты на выполнение вызова могут оказаться больше чем затраты на выполнение непосредственного кода функции, которая написана на встроенном ассемблере. Это хорошо демонстрирует пример функции Increment. Решением такой проблемы может быть использование встроенной подстановки (inline), которая позволяет в месте вызова подпрограммы (процедуры или функции), написанной на встроенном ассемблере, осуществить непосредственную вставку машинного кода, который соответствует этой подпрограмме. Таким образом, для выполнения встроенной подстановки показанной ранее функции Increment, необходимо просто добавить соответствующую директиву компилятора:

    
    
    with Interfaces;           use Interfaces;
    with Ada.Text_IO;          use Ada.Text_IO;
    with System.Machine_Code;  use System.Machine_Code;
    
    procedure Inc_It2 is
    
       function Increment (Value : Unsigned_32) return Unsigned_32 is
          Result : Unsigned_32;
       begin
          Asm ("incl %0",
               Inputs  => Unsigned_32'Asm_Input ("a", Value),
               Outputs => Unsigned_32'Asm_Output ("=a", Result));
          return Result;
       end Increment;
       pragma Inline (Increment);
    
       Value : Unsigned_32;
    
    begin
       Value := 5;
       Put_Line ("Value before is" & Value'Img);
       Value := Increment (Value);
       Put_Line ("Value after is" & Value'Img);
    end Inc_It2;
    

После компиляции этой программы с указанием выполнения полной оптимизации (опция -O2) и разрешением осуществления встроенной подстановки (опция -gnatpn) функция Increment будет откомпилирована как обычно, но в том месте, где раньше указывался вызов функции:

    
    
    pushl %edi
    call _inc_it__increment.1
    

теперь будет расположен непосредственный код этой функции:

    
    
    movl %esi,%eax
    #APP
       incl %eax
    #NO_APP
       movl %eax,%edx
    

В конечном итоге, это повысит общую эффективность выполнения кода программы.

9.3.10 "Затирание" содержимого регистров

До настоящего момента времени, мы явно указывали компилятору (посредством параметров ввода и вывода) какие регистры процессора мы используем, и генератор кода компилятора вынужден был это учитывать. Однако, что произойдет в том случае когда мы используем какой-либо регистр процессора, а компилятор об этом не знает? Можно догадаться, что в таком случае компилятор тоже будет использовать этот регистр процессора, а результатом этого будет какой-нибудь "сюрприз" при работе программы. Следовательно, использование в коде ассемблера какого-либо регистра процессора, о котором компилятору ничего не известно, может привести к каким-нибудь побочным эффектам в инструкциях (например, mull, сохраняющая результат своего действия в двух регистрах eax и edx) или в действиях, описанных в коде ассемблера, подобно следующему:

    
    
    Asm ("movl %0, %%ebx" &
         "movl %%ebx, %1",
         Inputs  => Unsigned_32'Asm_Input  ("=g", Input),
         Outputs => Unsigned_32'Asm_Output ("g", Output));
    

Здесь, компилятор не подозревает о том, что регистр ebx уже используется. В подобных случаях, необходимо использовать параметр Clobber, который дополнительно укажет компилятору регистры, используемые в коде ассемблера:

    
    
    Asm ("movl %0, %%ebx" &
         "movl %%ebx, %1",
         Inputs  => Unsigned_32'Asm_Input  ("=g", Input),
         Outputs => Unsigned_32'Asm_Output ("g", Output),
         Clobber => "ebx");
    

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

  1. именам регистров не должен предшествовать символ процента
  2. имена регистров в списке разделяются символом запятой: "eax, ebx"

Кроме перечисления используемых регистров в параметре Clobber, существует еще два именованых параметра которые удобно использовать в некоторых случаях:

  1. использование регистра cc указывает, что могут быть изменены флаги
  2. использование регистра memory указывает, что может быть изменено расположение в памяти

9.3.11 Изменяемые инструкции

Иногда, оптимизатор компилятора может перехитрить самого себя. Например, когда инструкция ассемблера с параметром ввода расположена внутри цикла, он может переместить загрузку параметра ввода за пределы цикла, подразумевая, что это одноразовая инициализация. Если ваш код не допускает такой трактовки (и вы обнаруживаете этот "сюрприз" достаточно быстро, после анализа генерируемого компилятором ассемблерного файла), то вы можете указать компилятору, чтобы он не пытался перехитрить вас путем установки в True параметра Volatile. Рассмотрим следующий пример:

    
    
    Asm ("movl %0, %%ebx" &
         "movl %%ebx, %1",
         Inputs   => Unsigned_32'Asm_Input  ("=g", Input),
         Outputs  => Unsigned_32'Asm_Output ("g", Output),
         Clobber  => "ebx",
         Volatile => True");
    

Примечательно, что по умолчанию параметр Volatile установлен в False пока не существует ни одного параметра вывода. В результате демонстрации такого примера, может показаться, что установку параметра Volatile в True стоит использовать повсеместно. Однако, необходимо заметить, что такая установка будет блокировать оптимизацию кода. Следовательно, ее использование реально необходимо только в тех случаях, когда без этой установки возникают какие-либо проблемы.

9.4 Синтаксис GNAT

В заключение обсуждения использования средств встроенного ассемблера представим полный синтаксис инструкции встроенного ассемблера GNAT, который непосредственно позаимствован из документации GNAT:

    
    
    ASM_CALL ::= Asm (
                     [Template =>] static_string_EXPRESSION
                   [,[Outputs  =>] OUTPUT_OPERAND_LIST      ]
                   [,[Inputs   =>] INPUT_OPERAND_LIST       ]
                   [,[Clobber  =>] static_string_EXPRESSION ]
                   [,[Volatile =>] static_boolean_EXPRESSION] )
    
    OUTPUT_OPERAND_LIST ::=
      No_Output_Operands
    | OUTPUT_OPERAND_ATTRIBUTE
    | (OUTPUT_OPERAND_ATTRIBUTE {,OUTPUT_OPERAND_ATTRIBUTE})
    
    OUTPUT_OPERAND_ATTRIBUTE ::=
      SUBTYPE_MARK'Asm_Output (static_string_EXPRESSION, NAME)
    
    INPUT_OPERAND_LIST ::=
      No_Input_Operands
    | INPUT_OPERAND_ATTRIBUTE
    | (INPUT_OPERAND_ATTRIBUTE {,INPUT_OPERAND_ATTRIBUTE})
    
    INPUT_OPERAND_ATTRIBUTE ::=
      SUBTYPE_MARK'Asm_Input (static_string_EXPRESSION, EXPRESSION)
    


Copyright (C) А.Гавва V-0.4w май 2004