День 22. Ассемблер. Windows и Linux. Компоновка ассемблерных программ
Автор статьи никого не призывает к правонарушениям и отказывается нести ответственность за ваши действия. Вся информация предоставлена исключительно в ознакомительных целях. Все действия происходят на виртуальных машинах и внутри локальной сети автора. Спасибо!
Оглавление цикла заметок о языке ассемблера
Сегодня продолжаю изучение языка ассемблера. Начну глубже разбираться с программированием под Windows и Linux. Помогает мне в этом книга Рудольфа Марека «Ассемблер на примерах». Когда-то Windows была лишь графической оболочкой для DOS (проходил на 17 день), но сейчас функции GUI встроены напрямую в ядро операционной системы, в отличие от UNIX-подобных систем (Linux, BSD и т.д.).
Взаимодействие Windows-приложений с ядром операционной системы происходит при помощи API-вызовов. Функции API имеют достаточно сложную структуру, поэтому даже простой «Hello, World» будет весьма объемным.
Функции API находятся в различных динамических библиотеках (DLL). Поэтому необходимо знать не только имя функции, но и библиотеку, в которой она находится.
Прежде чем начать, необходимо скачать файл win32n.inc
и поместить в папку include
программы SASM. Скачать этот файл можно здесь: http://rs1.szif.hu/~tomcat/win32/.
Пример из книги снова не зашел. Возникала ошибка «undefined reference to `MessageBoxA'
». Заменил на _MessageBoxA@16
, а перед ExitProcess
поставил нижнее подчеркивание. До сих пор не могу понять почему так и по какой причине в учебнике это рабочий пример (без символа подчеркивания и @16
в конце).
%include "io.inc"
%include "win32n.inc"
extern _MessageBoxA@16
extern _ExitProcess
SECTION CODE USE32 CLASS=CODE
global CMAIN
CMAIN:
mov ebp, esp; for correct debugging
push UINT MB_OK
push LPCTSTR title
push LPCTSTR banner
push HWND NULL
call _MessageBoxA@16
push UINT NULL
call _ExitProcess
xor eax, eax
ret
SECTION DATA USE32 CLASS=DATA
banner db 'Hello world!',0xD,0xA,0
title db 'Hello',0
Под 64-разрядный процессор эта программа выглядит следующий образом (ее уже публиковал в 19 день. Тогда получилась обратная ситуация – собирал 32 разрядную программу при помощи 64-разрядного компилятора):
global main
extern MessageBoxA ;from user32
extern ExitProcess ;from kernel32
section .text
main: push rbp ;sub rsp,40
xor r9d,r9d ;mov r9,MB_OK
mov r8d,title
mov edx,hello
xor ecx,ecx ;mov rcx,NULL
call MessageBoxA
xor ecx,ecx
call ExitProcess
hello: db 'Hello, Windows!',0
title: db 'My First Win64',0
Программирование под Linux
Сначала я хотел опустить этот раздел, так как подумал о небольшом отличие от программирования под Windows. Но думаю стоит остановиться на ELF – типичный формат для UNIX, следовательно, и Linux. Формат определяет порядок загрузки секций в память. Про секции было вчера.
По умолчанию программа загружается с адреса 0x08048000
. Сначала загружается одна страница. Остальные загружаются по мере необходимости.
Цитирую из учебника: «На диске программа хранится без секций .bss
и .stack
– эти секции появляются только тогда, когда программа загружается в память». Тогда вопрос, где хранятся эти секции, если не на диске?
В Linux ядро находится за пределами нашего адресного пространства. При вызове прерывания 0x80
изменяется значение сегментного регистра и процессор начинает выполнять код ядра. После обработки прерывания, управление передается нашей программе.
При работе с файлами в Linux не стоит забывать об особенностях UNIX-подобных операционных систем: у каждого файла есть права доступа, следовательно, у нашего процесса должны быть соответствующие полномочия.
Для отладки программ можно использовать отладчик gdb. Но он громоздкий и больше подходит для отладки Си-программ (нужно им позже попользоваться). Для языка ассемблера предлагается использовать отладчик ALD (Assembly Language Debugger).
Ассемблер GAS является родным для UNIX-подобных операционных систем. Он используется совместно с компилятором gcc, когда в коде Си-программ есть ассемблерные вставки. В отличие от NASM и других компиляторов, которые используются синтаксис Intel, GAS применяет синтаксис AT&T. Далее переходим к стыковке программ на языке ассемблера с другими программами.
Глава 13. «Компоновка – стыковка ассемблерных программ с программами, написанными на языках высокого уровня»
Языки высокого уровня поддерживают два способа передачи аргументов функциям: по ссылке и по значению. При передаче по значению, данные передаются в одном направлении, т.е. значение, переданное в функцию будет меняться только в ней и будет неизменным в точке передачи.
Во втором случае передается ссылка на переменную, изменение значения которой влечет обновление значения переменной в точке передачи.
Аргументы подпрограмм в языках высокого уровня всегда передаются через стек. Передача аргументов через регистры процессора недопустима.
Подпрограммы вызываются с помощью команды CALL
, а возврат из подпрограммы осуществляется с использованием команды RET
. Команда PUSH
помещает аргументы в стек.
Пространство для локальных переменных в стеке выделяется простым уменьшением указателя на вершину стека ESP
на необходимый размер.
Перед выходом из подпрограммы нужно освободить стек, позже выполнить команду RET
.
Стековый кадр (или стек-фрейм) предназначен для сохранения адреса возврата при вызове подпрограммы или восстановления значений регистра процессора. К локальным переменным доступ непосредственно через указатель вершины стека не совсем удобен из-за различной длины аргументов. Для решения этой проблемы отлично подходит регистр (E)BP
. Глубина вложенности подпрограмм может быть любой. Исходное значение (E)BP
указывает на предыдущий стековый кадр. Таким образом получается связанный список из кадров.
Вот и дошел до ответа на вопрос, связанный с подчеркиванием или его отсутствием перед функцией. Компиляторы (не все) Си добавляют символ подчеркивания к глобальным функциям, поэтому функция main()
для программиста на ассемблере доступна как _main()
. Исключением является формат ELF (символ подчеркивания не требуется).
В книге предлагается объявить функцию, а определение написать на языке ассемблера. Код на Си следующий (файл main.c):
#include "stdio.h"
const int plus = 6;
void printit(int);
int main(void) {
printit(5);
return 0;
}
Но для начала необходимо что-то скомпилировать на GAS. В интернете нашел следующий пример Hello, World’а:
.data
msg:
.ascii "Hello, world!\n"
.set len, . - msg
.text
.globl _start
_start:
# write
mov $4, %eax
mov $1, %ebx
mov $msg, %ecx
mov $len, %edx
int $0x80
# exit
mov $1, %eax
xor %ebx, %ebx
int $0x80
Чтобы его собрать в объектный файл, необходимо выполнить следующую команду, где main.s файл с кодом ассемблера:
$ as --32 main.s -o hello-main.o
Далее необходимо собрать в выполняемый файл под нашу архитектуру:
$ ld -melf_i386 -s hello-main.o -o hello-main
Спустя два часа мне все же удалось скомпоновать код ассемблера и Си. Окончательный код ассемблера ниже (файл myfile.asm) (Примечание: как оказалось не последний вариант. Итоговый будет ниже):
%include "misc/c32.mac"
global _main
extern plus
extern printf
global printit
section .text
_main:
proc printit
%$what arg
mov eax,[ebp + %$what]
add eax,[plus]
push eax
push strl
call printf
endproc
section .data
strl db 'SUM - %d.', 0x0A, 0x0
А теперь о подводных камнях. Библиотеки c32.mac
не было, поэтому ее скачал здесь: https://github.com/coapp-packages/nasm.
Далее пришлось закрыть SASM и перейти в консоль. Папку misc
из архива распаковал в корень папки NASM. Далее создаем объектный файл:
nasm -f elf32 myfile.asm
Переходим в Linux, где расположен код с объявлением функции printit()
на Си. Теперь необходимо скомпоновать нашу программу из двух частей:
gcc -m32 main.c myfile.o -o main -static
Казалось бы, все просто, но в учебнике Рудольфа Марека «Ассемблер на примерах», к сожалению, многое не написано. Поэтому оставлю ссылки на решение проблем, с которыми столкнулся.
Ответ Nick ODell на stackoverflow
The -m32 is telling gcc to compile for a 32-bit platform. On a 64-bit machine, gcc normally only comes with 64-bit libraries. You have two options: Install 32-bit headers and libraries. Here’s how you’d do this on Ubuntu.
Run this command:
sudo apt-get install gcc-multilib
Откуда и появился ключ -m32
:
gcc -m32 main.c myfile.o -o main -static
- Следующая проблема спровоцировала сражение с ветряными мельницами, а именно ошибка:
/usr/bin/ld: myfile.o: in function `_main':
myfile.asm:(.text+0x13): undefined reference to `_printf'
Как ранее понял, в 32-разрядном ассемблере необходимо добавлять подчеркивание к основным функциям из Си. Потом вспомнил, что для Linux (ELF формат) символ подчеркивая не требуется. Далее осознал, что функция main в myfile.asm не нужна (основная программа написана на Си). Символ подчеркивания в данном случае (_main
) исключал переопределение функции. Поэтому окончательный вариант файла myfile.asm следующий:
%include "misc/c32.mac"
extern plus
extern printf
global printit
section .text
proc printit
%$what arg
mov eax,[ebp + %$what]
add eax,[plus]
push eax
push strl
call printf
endproc
section .data
strl db 'SUM - %d.', 0x0A, 0x0
Ответ Oh-Ben-Ben на Stackoverflow
Когда Вы собираете Ваш проект, и хотите включить в него библиотеку(собранную Статически или Динамически *.a или *.so) происходит связывание ld всего Вашего кода. Когда вы где-то пишите, что тут будет вызываться функция библиотеки A, компилятор оставляет там пометку (по сути обещание), что референс на данные call будет подставлен на этапе линковки. Далее ликовщик смотрит на флаги связывания SHARED или STATIC(что и отвечает за динамическую или статическую библиотеку) и ищет ее согласно стандартным путям и/или указанным Вами путям.
Статическая библиотека — (*.a) собрана для непосредственного встраивания в Ваш исполняемый файл. Она просто будет помещена в соответствии с указанием linker’а. Тут будет статическая линковка.
Динамическая библиотека — (*.so) — будет просто подключаться как link на референс и не попадет в Ваш бинарь. Будет лишь указание где брать референс на тот или иной функционал. Тут будет динамическая линковка.
Раздел 13.4 книги Рудольфа Марека «Ассемблер на примерах» рассматривать не буду. Там идет речь о компоновке с Pascal-программой.
Вот и подошла к окончанию книга. Не скажу, что я разобрался с языком ассемблера, но точно заложил фундамент для дальнейшего изучения компьютерной безопасности. Позже вернусь к книге Майкла Сикорски «Вскрытие покажет! Практический анализ вредоносного ПО».
Теперь могу переходить к четвертому шагу (день 15) и разбираться с системами управления базами данных.