Главная » Заметки » День 22. Ассемблер. Windows и Linux. Компоновка ассемблерных программ
День 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) и разбираться с системами управления базами данных.

Просмотров: 62
29.06.2022
Автор