На написание этой статьи вдохновил этот замечательный пост: 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 подсветка.
Ресурсы:
