Ассемблер против C: Зачем изучать ассемблер?
В данной статье рассматриваются два языка программирования, а именно C и ассемблер, и показана необходимость знать язык ассемблера для программирования встраиваемых систем.
Язык ассемблера и рост количества недорогой памяти
В настоящее время программирование большинства встраиваемых систем выполняется на C; а если не на C, то на другом языке высокого уровня, таком как C ++.
Так было не всегда. В начале появления встраиваемых систем код писался на ассемблере; это был единственный вариант. В те дни память была чрезвычайно ограничена, поэтому требовался очень жесткий контроль за ее использованием, и ассемблер обеспечивал этот контроль. Но, кроме этого, не было доступных инструментов для языков высокого уровня.
Прошло несколько лет, прежде чем на рынке появились инструменты, и еще несколько лет, прежде чем их качество стало достаточно хорошим для разработки серьезного кода. Инструменты появились как раз в нужное время, поскольку процессоры становились всё более мощными (стали доступными 16-разрядные и 32-разрядные устройства), память становилась всё дешевле и плотнее, а сложность приложений увеличивалась.
Итак, что насчет сегодня? У нас есть чрезвычайно мощные процессоры, которые могут быть обеспечены огромными объемами памяти, с чрезвычайно сложными приложениями, которые разрабатываются большими командами программистов.
Где пригодятся навыки знания ассемблера?
Зачем учить ассемблер? Навыки программирования встраиваемых систем
На самом деле есть два навыка, каждый из которых может быть ценным: умение читать/понимать язык ассемблера и умение писать на нем.
Почему вы должны знать, как читать на ассемблере
У большинства разработчиков программного обеспечения встраиваемых систем должна быть возможность читать на ассемблере. Это необходимо по двум причинам.
Во-первых, эффективность кода во встраиваемой системе почти всегда важна. Современные компиляторы обычно отлично справляются с оптимизацией кода. Тем не менее, важно понимать, какие замечательные вещи сделал компилятор. В противном случае при отладке может возникнуть путаница.
Компиляторы, как правило, не просто переводят C на язык ассемблера. Хороший современный компилятор берет алгоритм, написанный на C, и выводит функционально эквивалентный алгоритм, написанный на ассемблере. Не то же самое. Вот почему отладка может быть сложной.
Также возможно, что компилятор не справился идеально – возможно, код C был написан не самым ясным образом – и разработчик должен быть в состоянии понять, что пошло не так. Проверка сгенерированного компилятором кода должна быть рутинной частью процесса разработки. Она дает возможность убедиться, что вывод компилятора действительно выполняет то, что задумал программист, и не был неправильно истолкован чрезмерно усердным оптимизатором.
Вторая причина, по которой некоторым разработчикам необходимо иметь возможность читать ассемблер, заключается в том, что это важно при программировании «близко к аппаратному обеспечению». В настоящее время драйверы не обязательно написаны 100% на ассемблере, но некоторое содержание на ассемблере почти неизбежно. Необходимо в подробностях понимать, что делает драйвер, чтобы использовать его наиболее эффективно и устранять неисправности.
Почему вы должны знать, как писать на ассемблере
Как насчет написания на ассемблере? В настоящее время было бы очень необычно, чтобы целое приложение было написано на ассемблере; большая часть кода, по крайней мере, написана на C. Итак, навыки программирования на C являются ключевым требованием для разработки встраиваемого программного обеспечения. Тем не менее, некоторые разработчики должны иметь представление о программировании на ассемблере. Конечно, этот навык специфичен для конкретного процессора; однако, если разработчик освоил язык ассемблера для одного процессора, переход на другой не должен быть слишком сложным.
Есть две причины писать на ассемблере. Первая и самая важная причина заключается в реализации некоторых функций, которые невозможно выразить на C. Простым примером может быть отключение прерываний. Этого можно достичь, написав подпрограмму на ассемблере и вызывая ее так, как если бы она была функцией C. Чтобы реализовать это, должен быть известен протокол вызова/возврата используемого компилятора C, но это, как правило, легко понять. Например, вы можете просто посмотреть на код, сгенерированный компилятором.
Другой способ реализовать код на ассемблере – вставить его в код C, как правило, используя ключевое слово расширения asm
. Это имеет смысл, когда требуется одна или несколько инструкций на ассемблере, поскольку устраняются накладные расходы на вызов/возврат. Реализация этого расширения варьируется от одного компилятора к другому, но обычно оператор asm
принимает такую форму:
asm(" trap #0");
Как правило, единственные места, где требуется функциональность, которая не может быть описана на C, – это код запуска и драйверы устройств. В этой части разработки встраиваемого программного обеспечения участвует небольшое количество разработчиков. Таким образом, потребность в навыках письма на ассемблере, как упоминалось выше, ограничена избранной группой инженеров.
Некоторые разработчики считают, что им нужно знать, как писать на ассемблере, чтобы реализовать код «более эффективным» способом, чем это сделает компилятор. Возможно, что в некоторых очень редких случаях они могут быть правы. Однако большинство современных компиляторов выполняют замечательную работу по оптимизации и генерации эффективного кода (имейте в виду, что «эффективный» может означать быстро или компактно – выбираете вы, хотя иногда вы можете получить и то и другое).
Вот пример:
#define ARRAYSIZE 4
char aaa[ARRAYSIZE];
int main()
{
int i;
for (i=0; i<ARRAYSIZE; i++)
aaa[i] = 0;
}
Это похоже на простой цикл, который устанавливает каждый элемент массива на ноль. Если вы скомпилируете это с разумным количеством активированной оптимизации и попытаетесь отладить код, вы получите странный результат: он будет перепрыгивать прямо через цикл (то есть будет вести себя так, как если бы цикла не было вообще). Это связано с тем, что компилятор определяет, что 32-разрядное перемещение нуля в массив будет выполнять работу намного эффективнее, чем цикл.
Результирующий код (в данном случае для процессора ARM) выглядит примерно так:
mov r3, #0
ldr r2, .L3
mov r0, r3
str r3, [r2]
bx lr
.L3:
.word .LANCHOR0
Изменение значения ARRAYSIZE
дает интересные результаты. Установка в значение 5 дает такой результат:
mov r3, #0
ldr r2, .L3
mov r0, r3
str r3, [r2]
strb r3, [r2, #4]
Еще нет цикла. При значении 8 продолжается в том же духе:
mov r3, #0
ldr r2, .L3
mov r0, r3
str r3, [r2]
str r3, [r2, #4]
Затем сборка этого кода для 64-разрядного процессора дает еще лучший результат:
mov w0, 0
str xzr, [x1, #:lo12:.LANCHOR0]
И так продолжается. Большие размеры массива приводят к созданию эффективных циклов или, возможно, просто к вызову библиотечной функции, такой как memset()
, стандартной библиотечной функции C, которую можно вызывать из ассемблера.
Суть в том, что навыки языка ассемблера далеко не устарели, но многие высококвалифицированные и очень продуктивные разработчики встраиваемого программного обеспечения могут быть ограничены в грамотном чтении кода на ассемблере.
Если вы хотите узнать больше о другой стороне этой концепции, посмотрите статью о языке C для программирования для страиваемых систем.
Поделитесь своими мыслями и опытом относительно использования языка ассемблера в комментариях ниже.