На написание этой статьи вдохновил этот замечательный пост: Learn x86-64 assembly by writing a GUI from scratch. Где-то здесь на Хабре даже был перевод, насколько я помню. Что-то как-то зудело от неё, как у мистера Монка.
-
Во-первых, непонятно, зачем писать на каждый чих отдельную функцию, если она будет за весь жизненный цикл приложения вызываться всего лишь один раз. Это лишний call/ret, увеличивающий размер.
-
Во-вторых, расположение запросов и ответов в стеке - ну, такое себе. Несмотря на комментарии в махинациях со стеком, конечно, можно разобраться, но довольно тяжко. Код должен быть красивым. Код должен быть понятен джуну.
-
В-третьих, если уж рисуем фонтом "fixed", то для этого совершенно необязательно делать OpenFont, X11 и так его выберет.
-
В-четвертых, просто захотелось попробовать уменьшить размер бинарника ещё больше.
Попробую описать, как мы писали на ассемблере более 30 лет назад. По-большому счету я больше читатель, чем писатель, да и писал на асме последний раз очень давно, так что прошу сильно тапками не кидаться. Уж как получится...
Перепишем все на fasmg, мне он как-то ламповее кажется, остальные более вырвиглазными что-ли. fasmg к тому же компилирует сразу в минимальный размер. Еще fasm обходится без линковщика. Плюс сможем написать кое-какие макросы в помощь.
Вначале идеи:
-
Для каждого запроса определим структуры, согласно спецификациям – сразу становится ясно и понятно для чего они и видно поля, которые они содержат.
-
Данные не будем хранить в бинарнике, чтобы полные структуры не занимали место – при помощи mmap выделим страницу памяти и будем туда все пихать (4kb хватит всем!). На самом деле памяти хватило бы еще меньше, но так уж работает mmap.
-
Дефолтные значения полей впишем в бинарник при помощи макроса. Последовательности с заполненными данными будем копировать при помощи movsb. Расположим структуры в памяти так, чтобы последовательности были как можно больше – этим сократим количество вызовов movsb.
-
Выделим несколько регистров для часто используемых переменных и не будем их менять.
-
Сделаем макрос для системных вызовов, чтобы каждый раз не расписывать регистры для каждого.
Кроме самого fasmg, нам еще понадобятся пакеты для него отсюда: [1]
Подготовительные работы. Писать будем в VSCode, поскольку, к сожалению, fasmw под Линукс не существует. Для начала, нам надо как-то компилировать код. Здесь: [2], есть пример таски для VSCode. Мы ее немного модифицируем, чтобы она каждый раз у нас не спрашивала параметры - в аргументах просто проставим конкретные значения. В "-i" параметре поставим DEBUG=0. Добавим еще еще одну таску - для chmod:
{
"label": "Make release executable",
"type": "shell",
"command": "chmod",
"args": [
"+x",
"${fileBasenameNoExtension}"
],
"dependsOn": "Release build with fasmg"
}
И еще одну таску, собственно для полной сборки:
{
"label": "Build Release and Make Executable",
"dependsOn": [
"Make release executable"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
Теперь по Ctrl+Shift+B можем скомпилировать код.
Повторяем все то-же замое, только заменяем "build" на "test" и DEBUG=0 на DEBUG=1. Это для отладки. С отладкой, конечно в fasm туговато, поскольку он не добавляет дебаг символы в бинарник. Хотя, вроде, есть возможность генерировать DWARF. Отлаживать будем в Cutter. Cutter не из каких-то принципиальных соображений, просто захотелось посмотреть, что он из себя представляет. Чтобы скомпилировать в режиме "отладки" жмем Ctrl+Shift+P и набираем "Open Keyboard Shortcuts" и пишем там:
[
{
"key": "ctrl+shift+t",
"command": "workbench.action.tasks.test",
"when": "editorTextFocus && resourceExtname == .asm"
}
На Ctrl+Shift+T будем собирать приложение в "отладочном режиме". Для самой отладки создаем launch.json:
{
"name": "Launch Cutter",
"type": "node",
"request": "launch",
"preLaunchTask": "Debug build with fasmg",
"runtimeExecutable": "Cutter.AppImage",
"runtimeArgs": [
"${fileDirname}/${fileBasenameNoExtension}",
"--analysis", "2",
"--arch", "x86",
"--bits", "64",
"--endian", "little",
"--base", "0x400000",
],
"console": "internalConsole"
}
Теперь об отладке - добавим что-то типа логирования, для этого создадим макрос на основе printf. Прилинковать libc в fasmg очень просто:
include 'dynamic/import64.inc'
if DEBUG
interpreter '/lib64/ld-linux-x86-64.so.2'
needed 'libc.so.6'
import printf
end if
Поскольку printf – функция с переменным количеством аргументов, то нам понадобится макрос для подсчета аргументов:
macro COUNT_ARGS_TO result, args&
iterate _, args
result = %
end iterate
end macro
% означает индекс в цикле, начиная с 1. И макрос для получения аргумента по индексу:
macro GET_VARARG_TO result, index, args*&
if index < 0
err 'invalid index'
else
local cnt
COUNT_ARGS_TO cnt, args
if index >= cnt
err 'invalid index'
end if
end if
iterate arg, args
if index = (% - 1)
result = arg
break
end if
end iterate
end macro
Здесь, проверяем, что индекс не выходит за пределы массива и находим аргумент по индексу.
Теперь чуть посложнее – нам нужно будет записать float/double аргумент в XMM{N} регистр, согласно System V ABI. Если мы хотим писать, что-то типа DEBUG_MSG 1.5, то нам как-то надо перевести 1.5 в IEEE-754. Для этого воспользуемся такой замечательной штукой, которая есть в fasm, как виртуальная память. Смысл в том, что в ней мы можем определить виртуальную память, в ней уже можем определить переменные, которые существуют только во время компиляции. Что нам это дает? Если мы напишем: dq 1.5, то fasm сам конвертирует 1.5 в IEEE-754. Выглядит это так:
macro MOVQ_IMM reg, val
local bits
virtual at 0
dq val
load bits qword from 0
end virtual
mov rax, bits
movq reg, rax
end macro
Мы получили double во время компиляции и сгенерировали код для запихивания его в регистр или стек.Теперь настало время организовать макрос с vararg (поддерживаем только %d, %f, %e, %g, %a, нам этого хватит):
VARG_FUNC
macro VARG_FUNC fn, format_string, args&
local real_format_string, skip_data, size, index, sword, founded, floatIdx, otherIdx, tmpArg
COUNT_ARGS_TO argsSz, args
jmp skip_data
real_format_string db format_string, 10, 0
real_format_string.size = $ - real_format_string
skip_data:
push rax
push rdi
push rsi
push rbp
mov rbp, rsp
and rsp, -16
index = 0
founded = 0
floatIdx = 0
otherIdx = 0
while index < real_format_string.size-1
load sword word from real_format_string + index
; Check for Floats (%f, %F, %e, etc)
if sword = '%' + ('f' shl 8) | sword = '%' + ('F' shl 8) |
sword = '%' + ('e' shl 8) | sword = '%' + ('E' shl 8) |
sword = '%' + ('g' shl 8) | sword = '%' + ('G' shl 8) |
sword = '%' + ('a' shl 8) | sword = '%' + ('A' shl 8)
GET_VARARG_TO tmpArg, founded, args
if floatIdx = 0
MOVQ_IMM xmm0, tmpArg
else if floatIdx = 1
MOVQ_IMM xmm1, tmpArg
else if floatIdx = 2
MOVQ_IMM xmm2, tmpArg
else if floatIdx = 3
MOVQ_IMM xmm3, tmpArg
else if floatIdx = 4
MOVQ_IMM xmm4, tmpArg
else if floatIdx = 5
MOVQ_IMM xmm5, tmpArg
else if floatIdx = 6
MOVQ_IMM xmm6, tmpArg
else if floatIdx = 7
MOVQ_IMM xmm7, tmpArg
else
err "Not ymplemented yet!"
end if
floatIdx = floatIdx + 1
index = index + 2
founded = founded + 1
else if (sword and $FF) = '%'
GET_VARARG_TO tmpArg, founded, args
if otherIdx = 0
mov rsi, tmpArg
else if otherIdx = 1
mov rdx, tmpArg
else if otherIdx = 2
mov rcx, tmpArg
else if otherIdx = 3
mov r8, tmpArg
else if otherIdx = 4
mov r9, tmpArg
else
err "Not ymplemented yet!"
end if
index = index + 2
founded = founded + 1
otherIdx = otherIdx + 1
else
index = index + 1
end if
end while
lea rdi, [real_format_string]
mov al, floatIdx
call [fn]
mov rsp, rbp
pop rbp
pop rsi
pop rdi
pop rax
end macro
Макрос получился примитивный, но для наших нужд сойдет. Код должен быть понятен.
На входе – vararg функция из libc, мы будем использовать printf, строка формата с ограниченной группой модификаторов и аргументы.
И мы готовы к макросу для логов:
macro DEBUG_MSG str_literal, args&
VARG_FUNC printf, str_literal, args
end macro
Структуры для API будут выглядеть так (не буду их расписывать, по ним все понятно):
struct FULL_LAYOUT
ridBase dd 0
ridMask dd 0
windowRootId dd 0
rootVisualId dd 0
gcId dd 0
exposed db 0
create_window_req X11_CREATE_WINDOW
draw_string_req STRING_TO_DRAW
handshake X11_HANDSHAKE
gc_request X11_CREATE_GC
addr_un SOCKADDR_UN
map_window_req X11_MAP_WINDOW
union
free rb HEAP_SIZE - ($ - create_window_req)
setup X11_SETUP
pollfd POLLFD
endu
ends
Поля расположены в порядке, который дает минимальный сгенерированный код.
Раскладку держим в r12, Здесь, отсутствуют socket, ..., держим их в регистрах r13+
Прекрасно. Теперь, поскольку лень расписывать регистры для системных вызовов, а лень, как известно – двигатель прогресса, то напишем макрос для них:
Системный вызов:
; syscall ABI: (rdi, rsi, rdx, r10, r8, r9). keep rcx & r11
macro MAKE_SYSCALL scall, args&
iterate arg, args
if % = 1
match =ptr [c], arg
lea rdi, [c]
else match [c], arg
mov rdi, [c]
else match =0, arg
xor rdi, rdi
else
mov rdi, arg
end match
else if % = 2
match =ptr [c], arg
lea rsi, [c]
else match [c], arg
mov rsi, [c]
else match =0, arg
xor rsi, rsi
else
mov rsi, arg
end match
else if % = 3
match =ptr [c], arg
lea rdx, [c]
else match [c], arg
mov rdx, [c]
else match =0, arg
xor rdx, rdx
else
mov rdx, arg
end match
else if % = 4
match =ptr [c], arg
lea r10, [c]
else match [c], arg
mov r10, [c]
else match =0, arg
xor r10, r10
else
mov r10, arg
end match
else if % = 5
match =ptr [c], arg
lea r8, [c]
else match [c], arg
mov r8, [c]
else match =0, arg
xor r8, r8
else
mov r8, arg
end match
else if % = 6
match =ptr [c], arg
lea r9, [c]
else match [c], arg
mov r9, [c]
else match =0, arg
xor r9, r9
else
mov r9, arg
end match
end if
end iterate
mov rax, scall
syscall
end macro
Еще нужно перенести данные в память. Берем полную расладку памяти и пробегаем побайтово. Подсчитываем количество непустых байтов идущих подряд. Как показала практика(метод научного тыка), минимальную длину лучше всего установить в 4 - это дает минимальный размер файла. Переписываем такие последовательности через MOVSB, остальные - каждый через MOV.
Макросы:
Раскладка в памяти
macro emit_data base_reg, addr_space, offset, seq_len
local data_seq, over_data
jmp over_data
data_seq:
repeat seq_len
load val:1 from addr_space:(offset + % - 1)
db val
end repeat
over_data:
lea rdi, [base_reg + offset]
lea rsi, [data_seq]
mov rcx, seq_len
rep movsb
end macro
macro INIT_LAYOUT base_reg, struct_type
local value, offset, next_offset, addr_space, seq_len, str_val
virtual at 0
addr_space::
instance struct_type
end virtual
push rcx rsi rdi
offset = 0
while offset < sizeof struct_type
load value:1 from addr_space:offset
if value <> 0
next_offset = offset + 1
while next_offset < sizeof struct_type
load value:1 from addr_space:next_offset
if value = 0
break
end if
next_offset = next_offset + 1
end while
seq_len = next_offset - offset
if seq_len > 4
emit_data base_reg, addr_space, offset, seq_len
offset = next_offset
else
load value:1 from addr_space:offset
mov byte [base_reg + offset], value
offset = offset + 1
end if
else
offset = offset + 1
end if
end while
pop rdi rsi rcx
end macro
Основной код теперь будет выглядеть так:
start:
; allocate one page memory
MAKE_SYSCALL SYSCALL_MMAP, 0, HEAP_SIZE, PROT_RW, MAP_MALLOC, -1, 0
mov r12, rax ; unlikly to fail
DEBUG_MSG "Allocated memory at: %p", r12
; prepare structures in allocated memory
INIT_LAYOUT r12, FULL_LAYOUT
virtual at r12
layout FULL_LAYOUT
end virtual
; open socket
MAKE_SYSCALL SYSCALL_SOCKET, AF_UNIX, SOCK_STREAM, 0
CHECK_ERROR error, close_socket
mov socket, rax
DEBUG_MSG "Socket created: %d", socket
...
По-моему красиво и все понятно. Размер исполняемого файла - 1237 байт. Размер сократился больше чем на треть. Очень неплохо. Проект на github: [3]
Все работает:
P.S. Конечно, по хорошему, надо делать проверки всех ошиюок и проверки всех ответов сервера.
P.P.S. К сожалению не смог подобрать нормальную подсветку синтаксиса, более-менее нормально выглядит только cpp подсветка.
Ресурсы:
Автор: horribile
