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

xtext
xtext

P.S. Конечно, по хорошему, надо делать проверки всех ошиюок и проверки всех ответов сервера.

P.P.S. К сожалению не смог подобрать нормальную подсветку синтаксиса, более-менее нормально выглядит только cpp подсветка.

Ресурсы:

  1. flat assembler g - examples library

  2. Using FASMG in Visual Studio Code

  3. Github project

  4. X Window System Protocol

  5. Searchable Linux Syscall Table

  6. x86-64 call convention