29. Связь Ассемблера и Си - Большие программы целиком на языке

Лекции по предмету «Информатика»
Информация о работе
  • Тема: 29. Связь Ассемблера и Си - Большие программы целиком на языке
  • Количество скачиваний: 83
  • Тип: Лекции
  • Предмет: Информатика
  • Количество страниц: 10
  • Язык работы: Русский язык
  • Дата загрузки: 2014-12-20 16:05:56
  • Размер файла: 22.07 кб
Помогла работа? Поделись ссылкой
Информация о документе

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

Если Вы являетесь автором текста представленного на данной странице и не хотите чтобы он был размешён на нашем сайте напишите об этом перейдя по ссылке: «Правообладателям»

Можно ли скачать документ с работой

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

29. Связь Ассемблера и Си

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

29.1. Вставка ассемблерного кода в текст программы на Си.
Рассмотрим пример. Нужно вычислить количество завершающих нулевых битов в двойном слове. Эта задача и ее интересные применения рассмотрены в книге [Алгоритмические трюки для программистов, с. 92-97]. Из этой книги выбрана наиболее простая версия кода, написанного на языке Си. Между тем эта задача очень просто решается с использованием одной из битовых команд, появившихся в 386 процессоре.
В тексте этой программы познакомимся с функций itoa (Integer TO ASCII). Эта функция не входит в стандарт языка Си, но реализована во многих средах программирования. Ее прототип находится в stdlib.h и выглядит так:
char *itoa(int value, char *string, int radix);
Функция преобразует значение value в позиционное представление этого числа по основанию radix. Представление является строкой символов и помещается в символьный массив string. Функция возвращает указатель на эту строку. Для строки надо зарезервировать достаточно места в памяти. Двоичное представление переменной типа int занимает максимально 32 байта. Еще один байт надо предусмотреть для терминатора строки.
Подчеркнем, что функция нестандартная. Например, в Visual C++ фирмы Microsoft эта имя этой функции начинается с символа подчеркивания: _itoa.

ntz.c
#include <stdio.h>
#include <stdlib.h>

int main() {
unsigned int number = 0x100, x, zerobits;
char string[33];

itoa(number, string, 2);
printf("number = %#x = %s
", number, string);

x = ~number & (number - 1);
zerobits = 0;
while (x != 0) {
zerobits++;
x >>= 1;
}
printf("1) zerobits = %u
", zerobits);
asm {
.386
bsf eax, number
jz kz
mov zerobits, eax
jmp short knz
kz: mov zerobits, 32
knz:
}
printf("2) zerobits = %u
", zerobits);
return 0;
}

Объяснять алгоритм, реализованный на Си, не будем: обратитесь к книге []. Ясно, что ассемблерная реализация намного проще и компактнее.
На примере этой программы видно, что ассемблерная вставка начинается с ключевого слова asm и окружена фигурными скобками. В командах, написанных на языке Ассемблера, можно использовать символические имена из программы на языке Си.
Заметим, что возможность ассемблерной вставки в программу на языке Си не стандартизирована. Например, в среде Visual C++ фирмы Microsoft ассемблерная вставка начинается с ключевого слова __asm (слово asm предваряют два символа подчеркивания).
Для трансляции такой программы в командной строке компилятора нужно указать две опции:
–B Compile via assembly (компиляция посредством ассемблирования);
–Exxx Alternate Assembler name (имя Ассемблера)
после опции –E нужно указать имя Ассемблера с «полным путем»:

C:prog>bcc32 –B –Ec: asmin asm.exe ntz.c
Borland C++ 5.2 for Win32 Copyright (c) 1993, 1997 Borland International
ntz.c:
Turbo Assembler Version 4.1 Copyright (c) 1988, 1996 Borland International

Assembling file: ntz.ASM
Error messages: None
Warning messages: None
Passes: 1
Remaining memory: 448k

Turbo Link Version 2.0.68.0 Copyright (c) 1993,1997 Borland International

Программа выводит:
number = 0x100 = 100000000
1) zerobits = 8
2) zerobits = 8

Файл ntz.c преобразуется во временный файл ntz.asm. Далее вызывается ассемблер tasm.exe, который создаёт объектный файл. Этот файл обрабатывается компоновщиком. Если возникают сомнения в корректности использования ассемблерных инструкция в программе на языке Си (например, не используются ли в ассемблерной вставке регистры, которые уже задействованы в коде Си-программы), полезно использовать ключ –S и смотреть ассемблерный код в целом.
Недостатки встроенного ассемблерного кода
• компилятор не оптимизирует код текста программы на Си,
• нет мобильности (нельзя перенести программу на другой тип процессора),
• медленнее выполняется компиляция,
• затруднена отладка.

Вызов функций, написанных на языке Ассемблера из Си-программ
Предположим, у нас есть файл с главной программой на языке Си prim.c и файл с подпрограммой на языке Ассемблера sub.asm. Их трансляция и компоновка из командной строки осуществляется командой
bcc prim.c sub.asm
Тогда для файла prim.c вызывается компилятор Си образуется файл prim.obj. Для файла sub.asm вызывается tasm.exe и образуется sub.obj. Наконец, вызывается компоновщик tlink, который из объектных файлов создаёт загрузочный (исполняемый) файл prim.exe.
Сделаем важное замечание. При трансляции программы на языке Ассемблера ее текст приводится к верхнему регистру и поэтому такие, например, имена, как Test и test неразличимы. Но в программе на языке Си такие имена различны. Поэтому при ассемблировании можно указать для tasm ключи, которые сделают имена различимыми.
Если запустить tasm.exe без хвоста командной строки, то на экран будет выведена краткая справка по ключам tasm. Одна из строк такова
/ml,/mx,/mu Case sensitivity on symbols: ml=all, mx=globals, mu=none
/mu — преобразование всех имён к верхнему регистру (по умолчанию)
/ml — имена различаются регистром (мнемоники команд и директив это не касается: Mov и mov неразличимы)
/mx — различие в регистре только для глобальных имён.
bcc автоматически вызывает tasm с ключом /ml. Если пользователь хочет, чтобы чувствительность к регистру была только для глобальных имён, то следует применить команды
tasm /mx sub.asm
bcc prim.c sub.obj
29.3. Передача параметров в функциях Си
Описание функции выглядит так
тип_результата имя_функции(тип_параметра_1, тип_параметра_2, …)
Соответственно, вызов функции имеет вид
[возвращаемое_значение = ] имя_функции(параметр_1, параметр_2, …);
Для дальнейшего изложения введём обозначение v = f(p1, p2,…, pn).
Вызывающая программа начинает формировать стековый кадр, помещая параметры в стек справа налево. При переходе в подпрограмму в стеке запоминается адрес возврата (в малой модели памяти это одно слово). В подпрограмме необходимо выполнить команды
push bp
mov bp, sp
После этого стековый кадр приобретает вид (в предположении, что каждый параметр занимает слово)
bp  старое BP
адрес возврата
bp + 4  p1
bp + 6  p2

bp + 2 + 2*n  pn
После выполнения тела подпрограммы нужно выполнить команды
mov sp, bp
pop bp
ret
Уничтожением стекового кадра занимается вызывающая программа. В ней выполняется команда
add sp, 2*n , где n — количество параметров.
Теперь вам должно быть понятно, почему, если мы хотим, чтобы подпрограмма изменила значения переменной, то в качестве параметра нужно использовать адрес этой переменной. Дело в том, что содержимое стекового кадра немедленно уничтожается, после того как функция отработала.
На рисунке изображён стековый кадр, где каждый параметр занимает слово. Можно передавать двойные слова и другие данные (например, плавающее число с двойной точностью занимает 8 байтов).
Для возвращаемого значения действует простое правило. Если возвращаемое значение слово, то результат в регистре AX, если двойное слово — в паре DX:AX. Исключение составляют плавающие числа. Рассматривать их не будем.
29.4. Вызов функций Си из программ на языке Ассемблера.
При выполнении задания D2 мы испытывали неудобство, связанное с тем, что приходилось вручную переводить числа из 16-ричного представления в двоичное, чтобы убедиться в правильности промежуточных и конечных результатов. Хорошо бы поместить в программу отладочную печать в двоичном коде. Можно написать свою подпрограмму на языке Ассемблера для получения строки нулей и единиц, соответствующих нужному числу, это несложно. Но ещё проще воспользоваться библиотечными программами Си.
Нам понадобятся две функции:
itoa( value, string, radix), (integer to ASCII) , которая записывает value в строку string представление числа value по основанию radix
int strlen( string) — возвращает длину строки
Вызов подпрограммы оформим в виде макроса.
.286 ; Разрешены инструкции 286-го процессора
INCLUDE macro.inc
outbin MACRO number
IF TYPE number EQ 0
IF number LE 0FFFFh
mov ax, number
ELSE
%OUT Constant number is too large
EXITM
ENDIF
ELSEIF TYPE number EQ 1
mov al, number
mov ah, 0
ELSEIF TYPE number EQ 2
mov ax, number
ELSE
%OUT Invalid operand number
EXITM
ENDIF
call out_number
ENDM
.MODEL small
.STACK 100h
GLOBAL _itoa:PROC, _strlen:PROC
.DATA
b DB 43h ; Выводимые числа
x DW 0FCD5h
w DD 1
num DB 19 DUP(0) ; 16 байтов на число
; и ещё 3 байта на завершение строки
.CODE
out_number PROC ; В AX - выводимое число
push di ; используем DI
push 2 ; Основание системы счисления
push OFFSET num ; Строка для вывода
push ax ; Выводимое число
call _itoa ; itoa( value, string, radix)
add sp,6
push OFFSET num
call _strlen ; int strlen( string)
pop cx
mov di, OFFSET num
add di, ax ; Переместить на конец строки
mov word ptr [di], 0A0Dh
inc di
inc di
mov byte ptr [di], $
message num
pop di
ret
out_number ENDP
start: mov ax,@data
mov ds,ax
outbin b
outbin x
outbin 123h
outbin w
exit
END start
Обратите внимание на использование символа подчёркивания перед именами библиотечных функций. В программе на языке Си подчёркиваний нет. Их автоматически добавляет компилятор. В этом можно убедиться, посмотрев ассемблерный файл для какой-нибудь программы на языке Си (воспользовавшись ключом -S), или в карте памяти.
Команды для получения exe-файла
tasm/mx outnum.asm
tlink -v outnum.obj,,,c:c5libcs
Присоединяется библиотека функций, предназначенных для малой модели памяти (на это указывает буква s в названии библиотеки).
Конечно, хотелось бы ещё воспользоваться функцией printf. Но здесь нас подстерегает неудача. Компоновщик сообщит нам
Error: Undefined symbol _ERRNO in module IOERROR
Error: Undefined symbol __REALCVTVECTOR in module REALCVT
Оказывается, для правильной работы printf нужно обязательно присоединять головной модуль, для малой модели памяти это c0s.obj. В нём и содержаться определения вышеупомянутых символов. Но мы не будем развивать эту тему.

29.5. Средства Ассемблера для интерфейса с языком Си.
Пример. Программа конкатенации двух строк. Сначала напишем головную программу test.c.
#include <stdio.h>
unsigned int ConStr( char*, char*, char*);
#define MAX_SIZE 50
int main() {
char Str1[MAX_SIZE], Str2[MAX_SIZE];
char FinStr[ 2 * MAX_SIZE];
unsigned int lenstr;

puts("Введите первую строку: ");
gets( Str1);
puts("Введите вторую строку: ");
gets( Str2);
lenstr = ConStr( Str1, Str2, FinStr);
puts("Объединённая строка: ");
puts( FinStr);
printf( "Длина строки: %u
", lenstr);
return 0;
}
А теперь программу на языке Ассемблера. Сначала нарисуем стековый кадр.
bp  старое BP
адрес возврата
bp + 4  Str1
bp + 6  Str2
bp + 8  FinStr
Файл constr.asm
.MODEL small
.CODE
GLOBAL _ConStr:PROC
_ConStr PROC
push bp
mov bp, sp
push si di
cld
mov di,@data
mov es,di
mov si, [bp+4] ; Адрес Str1 - в SI
mov di, [bp+8] ; Адрес FinStr - в DI
Str1Loop:
lodsb ; В AL - элемент строки
and al,al ; Конец строки?
jz DoStr2 ; Да - будем присоединять Str2
stosb ; Нет - перепишем в FinStr
jmp Str1Loop
DoStr2:
mov si,[bp+6] ; Адрес Str2 - в SI
Str2Loop:
lodsb
stosb
and al,al ; Конец строки?
jnz Str2Loop ; Нет - повторять копирование
mov ax,di ; Адрес терминатора строки - в AX
dec ax ; Терминатор не включать
sub ax, [bp+8] ; Определить длину строки
pop di si bp
ret
_ConStr ENDP
END

В программе на языке Ассемблера для нас были неприятные моменты:
• приходилось следить за подчёркиванием в начале глобальных имён, хотя в исходном модуле на Си этого не было,
• приходилось тщательно выписывать пролог и эпилог программы, хотя они, очевидно стандартны,
• приходилось рисовать стековый кадр и тщательно отслеживать смещения относительно bp, что чревато ошибками; хотелось бы использовать символические имена.
В TASM имеются средства для исправления положения.

Директива .MODEL.
Если в начале файла размещать директиву
.MODEL small,C
то можно не указывать символ подчёркивания. Пролог и эпилог в подпрограммах генерируется автоматически.

Директива PROC
Знакомая нам директива расширяется так:
имя PROC USES список_сохранямого_в_стеке
как правило, в стеке сохраняются регистры, их разделяют пробелами.
(Но если не указать .MODEL small,C то предупреждение
USES has no effect without language)

Директива ARG
В директиве ARG перечисляем параметры и их типы в порядке их расположения в списке формальных параметров, например
ARG Str1:word, Str2:word
Тогда вместо команды mov si, Str1 или mov si, OFFSET Str1 Ассемблер сгенерирует mov si, [bp+4]. Разумеется, при этом обязательно использование директивы .MODEL small,C.

Пример. Перепишем заново программу ConStr
.MODEL small,C
.CODE
GLOBAL ConStr:PROC
ConStr PROC USES si di
ARG Str1:word, Str2:word, FinStr:word
cld
mov di,@data
mov es,di
mov si,Str1
mov di,FinStr
Str1Loop:
lodsb
and al,al
jz DoStr2
stosb
jmp Str1Loop
DoStr2:
mov si,Str2
Str2Loop:
lodsb
stosb
and al,al
jnz Str2Loop
mov ax,di
dec ax
sub ax, OFFSET FinStr
ret
ConStr ENDP
END
Она стала намного проще для восприятия.
Перепишем программу для использования большой модели памяти. Теперь стековый кадр имеет вид (адрес возврата и параметры — двойные слова — сегмент:смещение)
bp  старое BP
адрес возврата

bp + 6  Str1

bp + A  Str2

bp + E  FinStr

Но благодаря использованию символических имен, можно не рассчитывать смещения. Зато загрузку указателей теперь придётся осуществлять с помощью команд lds и les.
файл constrl.asm
.MODEL large,C
.CODE
GLOBAL ConStr:PROC
ConStr PROC USES si di ds
ARG Str1:dword, Str2:dword, FinStr:dword
cld
lds si,Str1
les di,FinStr
Str1Loop:
lodsb
and al,al
jz DoStr2
stosb
jmp Str1Loop
DoStr2:
lds si,Str2
Str2Loop:
lodsb
stosb
and al,al
jnz Str2Loop
mov ax,di
dec ax
sub ax, OFFSET FinStr
ret
ConStr ENDP
END
Команда для создания exe-файла
bcc -ml test.c constrl.asm
Ключ -ml указывает на использование большой модели памяти.

29.6. Особенности интерфейса при использовании C++.
Первый шаг в переходе от C к C++ — изменить расширение файла с .c на .cpp. В результате файл будет обрабатываться компилятором C++, что повлечёт например более строгую проверку типов и т.д.
Проделаем это. Переименуем test.c в test.cpp. При компоновке нас ожидает неудача:
bcc test.cpp constr.asm

Turbo Link Version 3.0 …
Error: Undefined symbol ConStr(char near*,char near*,char near*) in module test.cpp
Для разгадки столь неожиданного сообщения сгенерируем ассемблерный файл и посмотрим его:
bcc -S test.cpp
В файле test.asm мы найдём строку
call near ptr @ConStr$qpzct1t1
Оказывается, компилятор C++ изменяет имена функций, добавляя в них закодированную информацию о типах параметров и возвращаемого значения. Поэтому компоновщик не нашёл этого глобального имени в модуле constr.obj.
Исправление несложно. В файле test.cpp изменим описание прототипа:
extern "C" unsigned int ConStr( char*, char*, char*);
Теперь компилятор будет генерировать внешнее имя по правилам языка Си, а не C++.

29.7. Интерфейс Turbo Pascal и Assembler.
Кратко, не приводя примеров, рассмотрим особенности интерфейса меду модулями, написанными на Паскале и Ассемблере.
1) Передача параметров происходит не как в Си — справа налево, а наоборот — слева направо.
2) Уничтожение стекового кадра возложено на подпрограмму и выполняется командой ret n, где n — число байтов в стековом кадре (разумеется, n — чётное число).
Эти правила носят название — паскалевское соглашение о связях. В языке Си имеются модификаторы, которые используются при описании прототипов функций:
int pascal f( p1, p2, …) — передача параметров по правилам языка Паскаль,
int cdecl f( p1, p2, …) — передача параметров по правилам языка Си.
Итак, если прототип функции Constr имеет вид:
unsigned int pascal ConStr( char*, char*, char*);
то в программе надо сделать следующие изменения:
во втором варианте изменить одну директиву
.MODEL small, pascal
а в первом варианте
• убрать символ подчёркивания у глобальных имён и записать имена большими буквами,
• заменить ret на ret 6,
• изменить ссылки на элементы стекового кадра: [bp+4] заменить на [bp+8] и наоборот.
При программировании для Win16 использовалось паскалевское соглашение о связях. Но в Win32 ввели новое соглашение о связях: модификатор __stdcall. Параметры передаются справа налево — как в Си, а уничтожение стекового кадра производится командой ret n.