ЗАМЕЧАНИЕ
Потребность взять определенные участки битов в слове может возникать при
взаимодействии с внешними устройствами ЭВМ, а также при решении задач
информационной безопасности.
Значение может иметь тип либо int, либо unsigned int и занимать от 1
до 16 бит. Поля размещаются в машинном слое в направлении от младших
к старшим разрядам. В полях типа int крайний левый бит — знаковый.
Если поле состоит ровно из одного бита, то это либо 0, либо –1. Поля не
могут быть массивами и не имеют адресов, поэтому к ним нельзя
применять операцию &. Пример:
117
/* битовое поле на Си */
struct prim
{
int a:2; /* два бита с назанием a, значения от -3 до 1*/
unsigned b:3; /* три бита с названием b, значения 0-8 */
int c:1;
} i; /* (справа налево) 0011000=24 */
Поля — не единственный способ выделения наборов битов в слове. Могут
использоваться также маскирование с помощью операции И — & (биты,
которые нам нужны, — значение разряда маски 1, не нужны — 0) и
побитовый сдвиг:
(с&24)>>3; /* наложили маску 0011000=24 и сдвинули на 3 вправо */
Смесь — переменная, которая в разное время может хранить объекты
различного типа и размера. Появляется возможность работы в одной и той
же области памяти с данными различного вида. Синтаксис следующий:
/* использование смеси в Си */
union k
{
int ik;
float fk;
char ck;
} z;
Переменная z должна быть достаточно велика для того, чтобы хранить
любой из трех типов. В один и тот же момент времени z может хранить
значение только одной из переменных-компонентов. Следующий пример
также иллюстрирует работу с битовыми полями и смесями.
/* Проба битовых полей и смесей */
#include
void printbin(int* ch)
{
int i;
for (i=15;i>=0;i--)
printf("%d", ((*ch)>>i) & 1);
printf("\n");
}
struct bits
{
unsigned a:3;
unsigned b:3;
118
unsigned c:4;
} sb;
union uni1
{
float f;
char c;
int k;
} u;
int main()
{
struct bits* uk=&sb;
u.f=12.5;
u.c='c';
u.k=5;
printf("
Значение смеси %f\n",u.f);
uk->a=5;
uk->b=5;
uk->c=0xB;
printf("Битовое поле = %d\n",uk->a);
printbin((int*) uk);
return 0;
}
Как уже отмечалось, язык Си — язык системного программирования.
Неудивительна поэтому легкость интеграции Си с ассемблером. Например,
во многих версиях Си-систем вполне допустимы прямые ассемблерные
вставки в программу по следующему принципу (значение команд
ассемблера приводится в соответствующем разделе данной книги):
/* ассемблерные вставки в программу на Си */
#include
#pragma inline
int main ()
{
int a=10,b=20,c;
print ("a= %d, b=%d\n",a,b);
119
asm mov ax,10; /* строка на ассемблере — занесение 10 в регистр
AX */
asm mul a; /* строка на ассемблере — команда умножения AX на a
*/
c=_AX; /* прямой доступ к регистру AX процессора, с = 100 */
print ("c= %d \n",c);
/* целый фрагмент на ассемблере */
asm { mov ax,a
mov bx,b
xchg ax,bx
mov a,ax
mov b,bx
}
print ("a= %d, b=%d\n",a,b);
return 0;
}
Как видно из примера (варианта реализации Си-системы), в программе на
Си возможно прямое обращение к регистрам процессора, при этом их
имена предваряются символом подчеркивания _ (для процессоров
архитектуры Intel x86 это _AX, _BX, _DX и пр.). Для вставки одной строки
на языке ассемблер достаточно написать в ее начале ключевое слово
asm
—
и все дальнейшее до знака перевода строки будет восприниматься
как строка ассемблера. Несколько строк можно вставить с помощью
конструкции asm {}. Чтобы подобные вставки стали допустимыми,
используется директива препроцессора #pragma inline. Изнутри
ассемблерных вставок переменные, объявленные в программе на Си,
остаются доступными просто по их именам (контроль типов —
применимости ассемблерных команд к данным — ложится на
программиста).
Контрольные вопросы и упражнения
1.
Что означает запись (*z)++? Как изменится значение переменной z?
Значение по адресу z?
2.
В чем разница между записями a[5] и *(a+5) при обращении к
элементу массива?
3.
Каким образом вы применили бы в программах указатели на функции?
Приведите пример.
4.
В какой ситуации нужны функции с переменным числом параметов?
5.
В каких ситуациях уместно использование битовых полей? Придумайте
пример.
120
6.
Возможен ли доступ из ассемблерной вставки к переменной Си?
7.
Возможен ли доступ в программе на Си к регистру процессора?
Достоинства и недостатки языка Си
Язык программирования Си был создан довольно давно — в 1971 году.
После появления он очень быстро завоевал популярность и потеснил такие
языки, как Фортран, Алгол и PL/I. Впоследствии язык Си сыграл
значительную роль в развитии языков программирования — стал
непосредственным прародителем или оказал влияние на такие языки, как
Objective C, C++, Java, C#, PHP, D и др.
Согласно рейтингу языков программирования TIOBE, на октябрь 2013
года чистый Си по популярности находится на первом (!) месте среди
языков программирования, опередив Java (второе место), С++ (четвертое)
и С# (шестое). Более того, шесть первых мест рейтинга заняты языками,
родственными Си!
В чем причина такого успеха?
Несомненными достоинствами языка программирования Си являются:
высокая скорость и компактность получаемых машинных программ —
эффективность, из-за которой язык широко используется при написании
встроенных приложений;
низкоуровневые возможности — также востребованы при создании
встроенных приложений и системных программ;
широкая известность и наличие компиляторов для очень большого
числа платформ.
В то же время следует еще раз отметить ряд особенностей языка
программирования Си, которые, на взгляд автора (несмотря на то что
обычно достоинства являются продолжениями недостатков и наоборот),
вполне можно считать его недостатками и которые ограничивают сферу
его успешного применения.
Конструкции Си изначально рассчитывались на профессионалов —
системных программистов, и поэтому текст программы как минимум не
вполне ясен начинающему программисту, а кроме этого он предоставляет
весьма широкие возможности — можно сказать: язык «остер, как
бритва» — можно порезаться! На Си допустим, к примеру, следующий
фрагмент программы (сочинен студентами МАИ):
int X;
X = 1;
X+=X++ + ++X; /* Догадайся, чему будет равен X? */
Си (и, увы, во многом произошедшие от него языки, хотя в них пытались
121
эту проблему решать — например, в Java запрещено в явном виде
использование указателей и отсутствует адресная арифметика) изобилует
потенциально небезопасными конструкциями. Так, в нем можно
применить присваивание вместо проверки на равенство внутри оператора
if
, например:
if (current->uid=0) retval=1;
и это не вызовет ошибки. Кстати, для борьбы именно с этой уязвимостью
был предложен метод записи условий в программах на Си, названный
нотацией Йоды. Этот персонаж саги «Звездные войны» необычным
образом строил предложения, меняя привычный порядок слов. При записи
if
в программе на Си в виде условия Йоды сначала пишется константа, с
которой производится сравнение, и лишь после знака — переменная:
if (0==current->uid) retval=1;
Естественно, присвоить числовой константе значение нельзя, и это вызовет
ошибку. Некоторые программисты считают хорошим тоном использовать
в программах на Си нотацию Йоды. Однако это не спасает ситуацию в
целом. Не вызывает сомнений, что Си – изобилующий потенциальными
опасностями и не вполне прозрачный для восприятия человеком язык. В
приложении А содержится позаимствованный из книги Павловской [12]
пример совершенно нечитаемой и тем не менее корректной для языка Си
программы. Поэтому Си нужно с осторожностью использовать для
начального
обучения
программированию.
Весьма
популярной
альтернативой для этого у нас в стране является Паскаль.
Из-за слабого контроля действий программиста (таких особенностей, как
ручное управление динамической памятью — отсутствия сборки мусора,
не существующей автоматической инициализации переменных, адресная
арифметика, отсутствия контроля выхода индекса за пределы массива, и
пр.) при программировании на Си потенциально совершается больше
ошибок, чем при программировании на Модуле-2, Обероне или Аде. Но
несмотря на это он, как кажется автору, неоправданно популярен при
создании критических приложений, например, в аэрокосмической
промышленности или автоматизированных системах управления
технологическими процессами.
Своеобразный промежуточный уровень языка Си, как считают многие
исследователи, не позволяет эффективно применять его при написании
приложений, требующих высокого уровня абстракции, например систем
искусственного интеллекта. Для этих областей гораздо лучше подойдут
Пролог или Лисп.
122
Язык ассемблера (автокод)
Как мы уже знаем, ассемблеры возникли в качестве первых отличных от
машинных кодов средств программирования и относятся к второму
поколению языков программирования. Какой же смысл сегодня, в XXI
веке, изучать эту древность? Или язык ассемблера — современное
средство? Разберем этот вопрос подробнее. Среди языков
программирования ассемблер ближе всего к архитектуре ЭВМ,
следовательно, он требует от программиста досконального знания деталей
реализации данной ЭВМ, особенностей архитектуры. Это позволяет писать
эффективные программы — короткие, быстрые, не требующие большого
объема памяти. Где они востребованы и востребованы ли? Ответ на этот
вопрос — востребованы, и эта ситуация будет наблюдаться, по всей
видимости, еще много лет.
Как бы ни показалось удивительным некоторым читателям, но существуют
применения ЭВМ, где доступные ресурсы до сих пор весьма ограниченны.
Это, например, встроенные микропроцессоры и микроконтроллеры. А
ведь именно они представляют собой наиболее многочисленный класс
современных ЭВМ, если подсчитать все используемые суперкомпьютеры,
мэйнфреймы, мини-ЭВМ (рабочие станции), персональные компьютеры
и т. д. Микроконтроллеры повсюду — в телевизорах, телефонах,
микроволновых печах, автомобилях, лифтах… Другой пример —
системные программы (ядро операционной системы, драйверы). Можно
привести в качестве примера также большие программные комплексы,
работающие в системах массового обслуживания, в которых имеется узкое
«бутылочное горлышко» — фрагмент программы, наиболее часто
выполняющийся и критический с точки зрения производительности
системы в целом. Такой фрагмент целесообразно писать на ассемблере,
даже если остальная программная система использует Java или C#.
Весьма важный аспект — информационная безопасность. Вирусы и
вредоносные программы, программы-шпионы используют бреши в
уязвимости компьютерных систем со знанием тончайших деталей и
особенностей архитектуры. Для того чтобы противостоять им, нужно
обладать не меньшими познаниями и искусством программирования на
уровне машины. Неудивительно, что умение программировать на
ассемблере — своеобразный знак качества программиста, а разработчики
подобных программ окружены ореолом таинственности как носители
тайного знания. Но это не каждому по плечу!
Рассматривать ассемблер мы будем на примере программирования
микропроцессоров Intel семейства x86. Данная архитектура не является ни
наиболее простой для изучения, ни наиболее удобной и оптимальной с
точки
зрения
разработки
программ.
Настоящие
ценители
123
программирования на ассемблере помнят изящество архитектуры
ассемблера ЭВМ разработки компании DEC семейства PDP-11 [13].
Причина выбора данного языка заключается в невероятном количестве
выпущенных по всему миру компьютеров, основанных на архитектуре
x86 —
впрочем, далеко не единственной выпущенной компанией Intel, в
послужном списке которой такие замечательные архитектуры, как,
например, iAPX 432 и Itanium. Из чего следует, что, во-первых, не должно
быть проблем с его нахождением и проверкой примеров, а во-вторых,
читатель, не исключено, сможет извлечь из материала некоторую
практическую пользу для себя. Ведь, как показывает многолетний опыт
преподавания, учащиеся не любят изучать новые языки и склонны в
практической работе на протяжении долгих лет использовать именно тот
язык программирования, который изучили на первом курсе... Ниже
представлены этапы исторического развития процессоров Intel.
Достарыңызбен бөлісу: |