Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Assembler / P29

.pdf
Скачиваний:
54
Добавлен:
02.06.2015
Размер:
362.18 Кб
Скачать

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\n", number, string);

x = ~number & (number - 1); zerobits = 0;

while (x != 0) { zerobits++;

x >>= 1;

}

printf("1) zerobits = %u\n", zerobits); asm {

.386

bsf eax, number

jz kz
mov zerobits, eax jmp short knz
mov zerobits, 32

kz:

knz:

}

printf("2) zerobits = %u\n", zerobits); return 0;

}

Объяснять алгоритм, реализованный на Си, не будем: обратитесь к книге []. Ясно, что ассемблерная реализация намного проще и компактнее.

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

Заметим, что возможность ассемблерной вставки в программу на языке Си не стандартизирована. Например, в среде Visual C++ фирмы Microsoft ассемблерная вставка начинается с ключевого слова __asm (слово asm предваряют два символа подчеркивания).

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

–B Compile via assembly (компиляция посредством ассемблирования);

–Exxx Alternate Assembler name (имя Ассемблера)

после опции –E нужно указать имя Ассемблера с «полным путем»:

C:\prog>bcc32 –B –Ec:\tasm\bin\tasm.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:\bc5\lib\cs

Присоединяется библиотека функций, предназначенных для малой модели памяти (на это указывает буква 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\n", lenstr); return 0;

}

А теперь программу на языке Ассемблера. Сначала нарисуем стековый кадр.

bp

 

bp + 4

 

bp + 6

 

bp + 8

 

Файл constr.asm

.MODEL small

.CODE

GLOBAL _ConStr:PROC

_ConStr PROC push bp

старое BP

адрес возврата

Str1

Str2

FinStr

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.

Соседние файлы в папке Assembler