November 11, 2005

ARM assembler & EVC++, часть первая и не последняя ;)

Про то, как использовать ARM ассемблер вместе с Embedded Visual C++.
Во-первых, забудьте про inline-ассемблер для ARM. Его в EVC4 нету. Вот для SH3 есть, для MIPS тоже есть, для X86, для PowerPC даже... Для ARM есть дизассемблер в debugger-е (и то немного кривоватый). А вот inline-ассемблера ARM нету... Но если уж очень сильно хочется вставить посреди функции пару "голых" инструкций ARM без линкования obj - то это все-таки можно сделать, правдо кривовато. Способ следующий: для начала нужно скомпилировать нужные строки в ARM Assembler-е. После этого дизассемблировать (лучше конечно-же в любимом IDA :) и записать байт-коды нужных комманд. Например, если мы хотим сделать

add r0, r0, r1

как пример, то его байт-код будет выглядеть как

01 00 80 E0

Теперь нужно вспомнить про то, что байты в числах записываются в перевернутом порядке - и сделать из байт-кода dword-число, которое мы вставим в код при помощи инструкции void __emit(unsigned const __int32), которая мало того, что не документирована, но еще и вставляет в готовый исполняемый файл произвольный набор байт :) Для нашего примера это будет

__emit(0xE0800001);

Извращенство, честное слово... Но работает :)

Для всех нормальных людей сущевствуют .obj файлы, которые можно спокойно линковать к вашей программе на C++ или C без плюсов. Вот про это и по-подробнее. Возьмем для начала функцию на C, которую мы хотим переписать на ассемблере. Например

DWORD __stdcall Addition(DWORD nFirst, DWORD nSecond){
    return nFirst + nSecond;
}

Простенько и со вкусом :) Теперь нужно определиться с тем, как передаются параметры и результат.
Несмотря на то, что указана директива __stdcall, при которой параметры как-бы должны передаваться через стек, по ходу дела Micro$oft на это дело забила, и использует стандартную для ARM передачу параметров и возвратных значений (xref to MSDN). А именно, 4 первых параметра передаются в регистрах r0, r1, r2, r3 соответственно, остальное передается в стеке. Ну в принципе, для нас это и хорошо - меньше возни со стеком :). Возвращается-же значение через регистр r0 (или r0 и r1, когда оно double или другое 64х-битное). С этим вроде все понятно. Но есть еще и регистры, значение которых неплохо-бы перед началом функции сохранять, а после окончания - восстанавливать (только не надо пожалуйста восстанавливать регистры r0 - r3 - я этого не переживу... :). Части кода, которые отвечают за сохранение регистров и их восстановление зовутся прологом и эпилогом (логично, правда? J). Miscor$oft в документации к ArmAsm приводит примеры прологов и эпилогов для ARM типа такого:

; Пролог для ARM с сохранением фрейма в r11, как предлагает
; это делать MS.
mov    r12, r13             ; Сохраняем sp.
stmdb  r13!, {r0-r3}        ; Эта строка откомментирована типа "так надо" 
                            ; (As needed). На самом деле это надо для того,
                            ; чтобы тело функции после пролога было немножечко
                            ; похоже на __stdcall функцию. Нам вобщем, не надо.
stmdb  r13!, {r4-r12, r14}  ; Сохраняем остальные регистры. Если какие-то
                            ; регистры у вас не используются - сохранять их не 
                            ; надо, чтобы зря процессор не гонять.
sub    r11, r12, #16        ; Устанавливаем фрейм после аргументов.

; ...
; Тело функции
; ...

; Эпилог - восстанавливаем регистры:
ldmdb  r11, {r4-r11, r13, r15}

Очень неплохо... Во-первых, нам не понадобится записывать первые 4 регистра в стек (с ними и так можно неплохо работать :), поэтому на две инструкции пролог можно спокойно сократить. Уже лучше. Во-вторых, заметили, что регистры r12 и r14 восстонавливаются в эпилоге в r13 и r15 соответственно? ;) Напомню, что r13 - это sp, r14 - lp (адрес возврата), а r15 - cp (аналог ip в x86). Догадались, наверное, уже. Мы одной строчкой убиваем двух зайцев, а третьего просто рвем на британский флаг: восстанавливаем основные регистры, восстанавливаем стек и возвращаемся в вызывавшую (callee) процедуру. Кстати, можно не сохранять r14 и потом его не восстанавливать, но тогда в конце добавится еще одна строчка:

mov    pc, lr               ; Вобщем, ret

С учетом всего вышенаписанного, наша процедура (т.е. функция) на ассемблере примет следующий вид:

; Пролог - сохраняем только sp и lr, т.к. кроме r0-r3 у нас ничего не
; используется тут бльше:
mov    r12, sp
stmdb  sp!, {r12, r14}      ; Сохраняем стек и адрес возврата.

; Собственно тело - складываем параметры в результат
add    r0, r0, r1           ; r0 = r0 + r1

; Эпилог.
ldmdb  r12, {r13, r15}      ; Восстанавливаем стек и делаем ret

Теперь, когда ассемблерный код в принципе-то написан, главный вопрос все-равно стоит. А именно, как это дело прикрутить к EVC++? Об этом следующий пост, ака часть вторая :)