Ассемблер в Linux для программистов C: различия между версиями

м
<source> -> <syntaxhighlight> (phab:T237267)
м (<source> -> <syntaxhighlight> (phab:T237267))
Вспомним, как вы писали <code>Hello, world!</code> на Си. Скорее всего, приблизительно так:
 
<sourcesyntaxhighlight lang="C">
#include <stdio.h>
#include <stdlib.h>
exit(0);
}
</syntaxhighlight>
</source>
 
Вот только <code>printf(3)</code> — функция стандартной библиотеки Си, а не операционной системы. «Чем это плохо?» — спросите вы. Да, в общем, всё нормально, но, читая этот учебник, вы, вероятно, хотите узнать, что происходит «за кулисами» функций стандартной библиотеки на уровне взаимодействия с операционной системой. Это, конечно же, не значит, что из ассемблера нельзя вызывать функции библиотеки Си. Просто мы пойдём более низкоуровневым путём.
А вот и сама программа:
 
<sourcesyntaxhighlight lang="C">
#include <unistd.h>
 
_exit(0);
}
</syntaxhighlight>
</source>
 
Почему <code>sizeof(str) - 1</code>? Потому, что строка в Си заканчивается нулевым байтом, а его нам печатать не нужно.
Давайте подумаем, каким будет результат выполнения следующего кода на Си:
 
<sourcesyntaxhighlight lang="C">
char x, y;
x = 250;
x = x + y;
printf("%d", (int) x);
</syntaxhighlight>
</source>
 
Большинство сразу скажет, что результат (250 + 14 = 264) больше, чем может поместиться в одном байте. И что же напечатает программа? 8. Давайте рассмотрим, что происходит при сложении в двоичной системе.
На Си это выглядело бы так:
 
<sourcesyntaxhighlight lang="C">
#include <stdio.h>
 
return 0;
}
</syntaxhighlight>
</source>
 
=== Команды сравнения и условные переходы. Безусловный переход ===
Сравните с кодом на Си:
 
<sourcesyntaxhighlight lang="C">
if(eax == 15)
{
}
/* а сюда управление перейдёт в любом случае */
</syntaxhighlight>
</source>
 
Кроме команд условного перехода, область применения которых ясна сразу, также существует команда безусловного перехода. Эта команда чем-то похожа на оператор <code>goto</code> языка Си. Синтаксис:
Этот код соответствует приблизительно следующему на Си:
 
<sourcesyntaxhighlight lang="C">
#include<stdio.h>
int main()
}
 
</syntaxhighlight>
</source>
 
Возможно, такой способ обхода массива не очень привычен для вас. В Си принято использовать переменную с номером текущего элемента, а не указатель на него. Никто не запрещает пойти этим же путём и на ассемблере:
Кто-то скажет: а ещё есть цикл <code>for()</code>! Но цикл
 
<sourcesyntaxhighlight lang="C">
for(init; cond; incr)
{
body;
}
</syntaxhighlight>
</source>
 
эквивалентен такой конструкции:
 
<sourcesyntaxhighlight lang="C">
init;
while(cond)
incr;
}
</syntaxhighlight>
</source>
И так-же:
<sourcesyntaxhighlight lang="C">
for(init; decr; )
{
body;
}
</syntaxhighlight>
</source>
Эквивалетен
<pre>
Эти сложные циклические сдвиги вам редко понадобятся в реальной работе, но уже сейчас нужно знать, что такие инструкции существуют, чтобы не изобретать велосипед потом. Ведь в языке Си циклический сдвиг производится приблизительно так:
 
<sourcesyntaxhighlight lang="C">
int main()
{
return 0;
}
</syntaxhighlight>
</source>
 
=== Подпрограммы ===
Далее, ищу в выводе препроцессора определение <code>dev_t</code>, нахожу:
 
<sourcesyntaxhighlight lang="C">
typedef __dev_t dev_t;
</syntaxhighlight>
</source>
 
Ищу <code>__dev_t</code>:
 
<sourcesyntaxhighlight lang="C">
__extension__ typedef __u_quad_t __dev_t;
</syntaxhighlight>
</source>
 
Ищу <code>__u_quad_t</code>:
 
<sourcesyntaxhighlight lang="C">
__extension__ typedef unsigned long long int __u_quad_t;
</syntaxhighlight>
</source>
 
Значит, <code>sizeof(dev_t)</code> = 8.
Мы бы могли и дальше продолжать искать, но в реальности всё немного по-другому. Если вы посмотрите на определение <code>struct stat</code> (<code>cpp /usr/include/sys/stat.h | less</code>), вы увидите поля с именами <code>__pad1</code>, <code>__pad2</code>, <code>__unused4</code> и другие (зависит от системы). Эти поля не используются, они нужны для совместимости, и поэтому в <code>man</code> они не описаны. Так что самый верный способ не ошибиться — это просто попросить компилятор Си посчитать это смещение для нас (вычитаем из адреса поля адрес структуры, получаем смещение):
 
<sourcesyntaxhighlight lang="C">
#include <sys/types.h>
#include <sys/stat.h>
return 0;
}
</syntaxhighlight>
</source>
 
На моей системе программа напечатала <code>sizeof = 88, offset = 44</code>. На вашей системе это значение может отличаться по описанным причинам. Теперь у нас есть все нужные данные об этой структуре, пишем программу:
Этот код эквивалентен следующему коду на Си:
 
<sourcesyntaxhighlight lang="C">
#include <stdio.h>
 
return 0;
}
</syntaxhighlight>
</source>
 
Смотрите: в секции <code>.rodata</code> (данные только для чтения) создаётся массив из 4 значений. Мы обращаемся к нему как к обычному массиву, индексируя его по <code>%eax</code>: <code>jump_table(,%eax,4)</code>. Но зачем перед этим стоит звёздочка? Она означает, что мы хотим перейти по адресу, содержащемуся в памяти по адресу </code>jump_table(,%eax,4)</code> (если бы её не было, мы бы перешли по этому адресу и начали исполнять массив <code>jump_table</code> как код).
Рассмотрим такой код на языке Си:
 
<sourcesyntaxhighlight lang="C">
if(((a > 5) && (b < 10)) || (c == 0))
{
do_something();
}
</syntaxhighlight>
</source>
 
В принципе, булево выражение можно вычислять как обычное арифметическое, то есть в такой последовательности:
Такой способ вычисления называется полным. Можем ли мы вычислить значение этого выражения быстрее? Смотрите, если <code>c == 0</code>, то всё выражение будет иметь значение true в любом случае, независимо от <code>a</code> и <code>b</code>. А вот если <code>c != 0</code>, то приходится проверять значения <code>a</code> и <code>b</code>. Таким образом, наш код (фактически) превращается в такой:
 
<sourcesyntaxhighlight lang="C">
if(c == 0)
{
 
dont_do_it:
</syntaxhighlight>
</source>
 
В принципе, можно пойти дальше: если <code>a <= 5</code>, нас не интересует сравнение <code>b < 10</code>: всё равно выражение равно false.
 
<sourcesyntaxhighlight lang="C">
if(c == 0)
{
 
dont_do_it:
</syntaxhighlight>
</source>
 
Такой способ вычисления выражений называется сокращённым (от англ. short-circuit evaluation), потому что позволяет вычислить выражение, не проверяя всех входящих в него подвыражений. Можно вывести такие формальные правила:
В принципе, сокращённое вычисление булевых выражений помогает написать более быстрый (а часто и более простой) код. С другой стороны, возникают проблемы, если одно из подвыражений при вычислении вызывает побочные эффекты (англ. side effects), например вызов функции:
 
<sourcesyntaxhighlight lang="C">
if((c == 0) || foo())
{
do_something();
}
</syntaxhighlight>
</source>
 
Если мы используем сокращённое вычисление и оказывается, что <code>c == 0</code>, то функция <code>foo()</code> вызвана не будет, потому что от её результата значение выражения уже не зависит. Хорошо это или плохо, зависит от конкретной ситуации, но, без сомнения, способ выполнения такого кода становится не очевидным.
583

правки