Если вы начинали изучение программирования с JavaScript, Rust, C или любого другого высокоуровневого языка, то ассемблерный код может показаться вам непонятным или даже пугающим.

Рассмотрим следующий код:

section .data
  msg db "Hello, World!"

section .text
  global _start

_start:
  mov rax, 1
  mov rdi, 1
  mov rsi, msg
  mov rdx, 13
  syscall

  mov rax, 60
  mov rdi, 0
  syscall

К счастью, по второй строке мы можем понять, что он делает.

Здесь нет ничего привычного нам: мы не видим ни условных операторов, ни циклов, нет никакого способа создавать функции… Да даже у переменных нет имён!

С чего же вообще начать?

Это небольшое введение предназначено для того, чтобы познакомить имеющих опыт в программировании с миром ассемблера. Мы обсудим основы языка и сопоставим их с конструкциями высокоуровневого программирования.

Завершив прочтение этого руководства, вы сможете ориентироваться в ассемблерном коде, будете знать, где искать информацию, и даже сможете самостоятельно писать простые программы.

Приступим!

Hello world


Разумеется, первой программой будет «Hello World».

Но прежде чем углубляться в код, нам нужно вкратце познакомиться с языком. К концу этого раздела мы уже сможем написать и запустить нашу первую ассемблерную программу.

▍ Ассемблер x86-64


Начнём сначала: ассемблер — это не язык.

Ассемблер — это семейство языков программирования, содержащее команды, очень схожие с машинным кодом, который исполняет CPU. Одна из причин существования ассемблерных языков — это создание человекочитаемой версии машинного кода в таких ситуациях, как реверс-инжиниринг, программирование оборудования или разработка игр для консолей.

В этом руководстве я буду использовать ассемблер x86-64, который можно собрать и исполнить на большинстве персональных компьютеров. Такой выбор упростит запуск кода и эксперименты с ним.

По историческим причинам существует две разновидности синтаксиса ассемблера x64-64: один называется Intel, другой — AT&T.

В этом руководстве мы будем использовать диалект Intel, потому что он используется в Intel Software Developer Manuals (SDM) — источнике достоверных данных о том, что на самом деле происходит в CPU, когда ему передаётся команда.

Работа с ассемблером означает близость к оборудованию. Оптимизация примеров кода с целью его портируемости между операционными системами и архитектурами привела бы к запутыванию содержания нашего введения.

Мы будем писать код для Linux, и он также должен нормально выполняться в Windows WSL. Общие концепции и практики остаются применимыми вне зависимости от используемой операционной системы.

▍ Анатомия команды


Команды позволяют приказывать CPU выполнять действия. Они выглядят примерно так:

mov rax, rbx

Они представляют собой наименьшую единицу ассемблерного языка и чаще всего состоят из двух частей:

  • мнемоники: сокращённого слова или предложения, описывающего выполняемую операцию,
  • операндов: списка из 0-3 элементов, описывающих то, на что влияет операция.

В нашем примере мнемоника — это mov, что расшифровывается как move, а операнды — это rax и rbx. Эта команда в текстовом виде означает, что нужно записать содержимое rbx в rax.

Примечание

rax и rbx — это регистры, мы познакомимся с ними в следующем параграфе. Пока можете представить, что это переменные, в которых хранятся значения.

У некоторых команд есть не только мнемоники и операнды. Позже нам понадобятся префиксы и директивы размера, поэтому мы поговорим о них в подходящий момент.

Не бойтесь, пока не нужно запоминать все команды. Когда мы будем сталкиваться с новыми операциями, мы будем обсуждать их, а благодаря повторению вы быстро их запомните.

Intel Software Developer Manuals (SDM) будет нашим справочным руководством для следующих глав. Держите его под рукой!

▍ Хранение данных: регистры


Регистры можно рассматривать как пространство для хранения, находящееся прямо в самом CPU. Они маленькие, а доступ к ним невероятно быстр.

Самые часто используемые регистры — это так называемые регистры общего назначения (general purpose). В x86-64 их всего шестнадцать, и каждый из них имеет ширину 64 битов.

Можно получить доступ ко всему регистру или его подмножеству при помощи разных имён. Например, указав rax (как в примере выше), мы будем адресовать все 64 бита в регистре rax. При помощи al, можно получить доступ к младшему байту того же регистра.

Регистр Старший байт Младший байт Младшие 2 байта1 Младшие 4 байта2
rax ah al ax eax
rcx ch cl cx ecx
rbx bh bl bx ebx
rdx dh dl dx edx
rsp spl sp esp
rsi sil si esi
rdi dil di edi
rbp bpl bp ebp
r8 r8b r8w r8d
r9 r9b r9w r9d
r10 r10b r10w r10d
r11 r11b r11w r11d
r12 r12b r12w r12d
r13 r13b r13w r13d
r14 r14b r14w r14d
r15 r15b r15w r15d
1: 2 байта иногда называют словом (word; отсюда и суффикс w)
2: 4 байта иногда называют двойным словом (double-word, или dword; отсюда и суффикс d)


«Общего назначения» означает, что регистры могут хранить всё, что угодно. Мы увидим, что на практике некоторые регистры имеют особое значение, некоторые команды используют только конкретные регистры, а некоторые стандарты определяют, кто может выполнять в них запись.

Единственный регистр не общего назначения, который нам будет интересен — это rip, или регистр указателя команд. В нём хранится адрес следующей для исполнения команды, а потому изменение значений в rip позволяет программам переходить к произвольным командам в коде.

▍ Наш первый ассемблерный файл


Ассемблерные файлы обычно имеют расширение .s или .asm и разделены на три части:

  • data: здесь мы определяем константы и инициализированные переменные,
  • bss: здесь мы определяем неинициализированные переменные,
  • text: здесь мы вводим наш код, это единственная обязательная часть файла.

section .data
  ; здесь константы

section .bss
  ; здесь переменные

section .text
  ; здесь код

Примечание

Точка с запятой (;) — это символ комментария: то, что идёт после него, не будет исполняться.

Ассемблерные программы работают вполне ожидаемым образом. Они начинаются с первой команды, а затем последовательно выполняют одну команду за другой, сверху вниз. Для создания потока управления, например, условных операторов и циклов мы заставляем программы переходить к конкретным командам. Подробнее переходы мы рассмотрим в следующих разделах.

Точно так же, как мы бы использовали функцию main во многих языках высокого уровня, ассемблер требует указать точку входа для нашей программы. Это можно сделать при помощи объявления global, указывающего на метку.

Метки в ассемблере позволяют присвоить конкретным командам человекочитаемые имена. Они решают две задачи: повышают понятность кода и позволяют нам ссылаться на эти команды из других частей программы. Объявить метку можно, написав её имя и добавив двоеточие: label:. Когда мы хотим сослаться на метку (например, в команде перехода), то её нужно использовать без двоеточия: label.

Обычно global ссылается на метку _start, объявляемую непосредственно после него. Именно отсюда наша программа начинает исполнение.

section .data
  ; здесь константы

section .bss
  ; здесь переменные

section .text
  global _start
_start:
  ; здесь команды

▍ Наконец-то «Hello World»


Итак, теперь у нас есть все инструменты для создания ПО на ассемблере.

Наша программа будет использовать два системных вызова: sys_write для вывода символов в терминал и exit для завершения процесса с указанным кодом состояния.

Системные вызовы (syscall) используются следующим образом:

  • выбираем вызываемый системный вызов, записав его идентификатор в rax,
  • передаём аргументы системного вызова, выполняя запись в соответствующие регистры,
  • используем команду syscall для запуска вызова.

Единственная дополнительная команда, которую мы будем использовать — это mov, с которой мы познакомились выше. Она устроена практически как присваивание (оператор =) в языках высокого уровня: копирует содержимое второго операнда в первый.

Давайте взглянем на код. (Весь код введения можно найти в репозитории shikaan/x86-64-asm-intro.)

Код пошагово прокомментирован. Внимательно изучите комментарии!

section .data
  ; Определяем константу `msg`, то есть строку для печати.
  ; Используем директиву`db` (define byte) для определения
  ; констант одного или нескольких байтов (1 символ = 1 байт).
  msg db `Hello, World!\n`

section .text
  global _start
_start:
  ; Выполняем системный вызов sys_write для вывода
  ; на экран. Системные вызовы выполняются при помощи `syscall`,
  ; они идентифицируются числом.
  ;
  ; Идентификатор sys_write - это число 1. Команда `syscall`
  ; обратится к идентификатору команды
  ; в регистре `rax`. Поэтому мы запишем туда 1.
  mov rax, 1
  
  ; Выполняемый системный вызов имеет следующую сигнатуру:
  ;
  ;   size_t sys_write(uint fd, const char* buf, size_t count)
  
  ; Команде `syscall` нужен первый аргумент
  ; из `rdi`. В данном случае первый аргумент -
  ; это дескриптор файла, в который нужно выполнять вывод.
  ; Мы используем 1, что обозначает стандартный вывод.
  mov rdi, 1
  
  ; Второй аргумент должен находиться в `rsi`,
  ; а сигнатура даёт нам понять, что это строка, которую
  ; нужно вывести. Мы определили буфер в разделе .data
  ; (это `msg`), поэтому нужно просто скопировать его в `rsi`
  mov rsi, msg
  
  ; `rdx` - это регистр для третьего аргумента. 
  ; Из сигнатуры мы видим, что это количество символов,
  ; которые мы хотим вывести.
  ;
  ; Примечание: строка завершается нулевым символом, то есть
  ; нам нужно учитывать "невидимый" символ в конце.
  mov rdx, 14
  
  ; Теперь мы, наконец, запускаем системный вызов
  syscall
  ; Сообщение выведено! Можно выходить из программы.

  ; Как и раньше, мы вызываем `syscall` с 60, что обозначает 
  ; `exit` и имеет следующую сигнатуру:
  ;
  ;   void exit(int status)
  
  ; `syscall` снова ищет идентификатор
  ; в `rax`. Мы запишем туда 60, то есть идентификатор `exit`
  mov rax, 60
  
  ; Первый аргумент снова должен
  ; находиться в `rdi`.
  ; Первый аргумент - это код состояния (см. сигнатуру),
  ; поэтому для выхода без ошибок мы записываем 0.
  mov rdi, 0
  
  ; Запускаем системный вызов `exit`
  ; и выходим из программы без ошибок
  syscall

Заключение


Мы написали «hello world»!

В этой первой статье мы изучили основные концепции ассемблера, попробовали на практике его синтаксис и даже написали работающее ПО. Более того, мы изучили, как взаимодействовать с операционной системой и готовы писать более интересные программы с условными операторами.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (37)


  1. Akina
    18.09.2024 13:21
    +1

    Итак, теперь у нас есть все инструменты для создания ПО на ассемблере.

    В этой первой статье мы изучили основные концепции ассемблера, попробовали на практике его синтаксис и даже написали работающее ПО.

    Вот прямо вижу: "Я переписал весь код в Блокноте и сохранил в файл C:\asm\HelloWorld.asm - но не работает! даже не запускается...".

    Если не считать этой досадной мелочи, а также того что напрямую к SDM не обратиться, ибо пошлёт, то всё остальное - ясно и понятно.


    1. chnav
      18.09.2024 13:21

      Не стреляйте в пианиста, это же перевод ))


  1. vk6677
    18.09.2024 13:21
    +5

    В ассемблере нет по умолчанию "невидимого" завершающего нуля, как Вы пишете. Так что на байт больше передали в системный вызов. У некоторых диалектов ассемблера есть .asciz директива, она добавляет 0 автоматически.

    Системный вызов wtite не требует завершающего нуля, только количество байт для записа.


    1. SquareRootOfZero
      18.09.2024 13:21

      Там передали 14 байт, и в строке, вроде, 14 символов, с учётом '\n'.


      1. vk6677
        18.09.2024 13:21

        Вы правы. Я не считал байты. Прочитал комментарий к коду. Тем более в начале статьи немного по-другому эта строка записана.


        1. SquareRootOfZero
          18.09.2024 13:21

          Количество байт передано правильное, а вот комментарий неправильный. null-terminated строки ведь не универсальны и не возникают в любом языке сами по себе.


  1. SquareRootOfZero
    18.09.2024 13:21
    +7

    Здесь нет ничего привычного нам: мы не видим ни условных операторов, ни циклов, нет никакого способа создавать функции…

    В смысле, а на высокоуровневых языках вроде JavaScript, Rust, C мы бы в hello world увидели условные операторы, циклы и создание функций?

    Да даже у переменных нет имён!

    А msg?


    1. SIISII
      18.09.2024 13:21

      ну, какую-нибудь функцию вроде main() на языке высокого уровня мы, скорей всего, увидели б. а msg -- это не переменная, это метка (имя для адреса памяти, а как сей адрес используется и что в той памяти -- про это msg ничего не знает)


      1. SquareRootOfZero
        18.09.2024 13:21
        +2

        На Python или Javascript мы бы запросто могли не увидеть никакой функции main(). А что "не переменная, а имя для адреса памяти" - это всё казуистика, и в C какая-нибудь переменная типа void* тоже, по-сути, имя для адреса памяти. Да, собственно, и не-void указатели - почти то же самое, только ещё сбоку присобачен небольшой хинт для компилятора: "пока думай, что по этому адресу лежит unsigned long double".


        1. SIISII
          18.09.2024 13:21

          Насчёт функций в скриптовых языках Вы, надо полагать, правы. А вот насчёт казуистики -- нет. Переменная A, объявленная как void *A, предназначена для хранения указателя (адреса) -- а соответственно, указывает не просто адрес, а область памяти строго определённого размера (соответствует размеру адреса), предназначенную для определённого применения -- хранения указателя на void. В языках более высокого уровня, чем Си/Си++, с именами переменных ещё больше вещей может быть связано -- связанных, например, с необходимостью динамически менять тип значения переменной в зависимости от того, что ей присвоено. Ассемблерная же метка -- это имя для адреса памяти и, как правило, ничего больше -- хотя размер, связанный с меткой, в некоторых ассемблерах встречается. Использовать переменную A для хранения чего-то, кроме указателя на void, можно, но лишь с помощью специальных "извращений" (операций приведения типа), ну а метка в ассемблере в принципе не накладывает никаких ограничений на её использование: хочешь -- читай или записывай данные произвольного размера, используя её в качестве адреса, хочешь -- передавай на неё управление, как будто там лежит код, хочешь -- используй её как часть выражения для вычисления какого-то другого адреса... Низкий уровень потому и низкий, что не строит абстракций над битиками и байтиками, оставляя это языкам высокого уровня, и в этом смысле переменных как таковых в ассемблере действительно нет -- есть лишь области памяти, иногда носящие имена, а иногда и не имеющие таковых.


          1. SquareRootOfZero
            18.09.2024 13:21

            Насчёт функций в скриптовых языках Вы, надо полагать, правы.

            И не только в "скриптовых" (что бы это ни значило) - Лисп, насколько я знаю, к таковым обычно не относят, а вотъ:

            (format t "Hello, World!")

            А вот насчёт казуистики -- нет.

            Выглядит как переменная, ведёт себя как переменная, используется как переменная - но не переменная. Можно перечислить много сходств с переменными в других языках, можно найти много различий, объявить какие-то из них принципиальными, провести через них тонкую красную линию: вот-де, тут у нас уже переменная, а тут ещё имя области памяти - что это всё, если не казуистика? В языках с динамической строгой типизацией - том же Python - насколько я помню, считается, что переменная сама по себе не имеет типа, а указывает на объект произвольного типа где-то в памяти - тоже, получается, не переменная, а метка?


    1. Wesha
      18.09.2024 13:21
      +3

      Более того, собственно переменных нет! И констант нет! Ничего нет!

      Я это описываю так:

      Память комьютера — это огромная «шахматная доска», в каждой клетке которой лежит число. В каких‑то из них могут лежать числа, которые нас по той или иной причине интересуют (например, потому что мы сами их туда положили), в остальных — лежат случайные числа («мусор»), но мы в них и не смотрим. Компьютер может найти каждую конкретную клетку по её номеру. Например, клетку № 153 422 731 мы отведём под число, которое будет счётчиком цикла. Но поскольку нам западло помнить такое большое и бессмысленное число, мы себе в табличку запишем, что i = 153422731 и будем везде использовать i. А потом, когда нужно будет готовить двоичный код для компьютера, везде, где мы видим «переменную i», мы будем производить подстановку. Соответственно, в какой‑то момент времени умный человек догадался, что имеет смысл научить сам компьютер вести такую табличку — увидев новое мнемоническое «имя переменной», автоматически находить неиспользуемую ячейку памяти («клетку на доске») и записывать в табличку рядом с именем переменной адрес этой ячейки, а потом при компиляции программы выполнять такую замену (имени — на адрес).


      1. perfect_genius
        18.09.2024 13:21
        +1

        Только не показывайте это новичкам, иначе им взорвёт мозг. Тут каждое предложение вызывает вопросы.


        1. Wesha
          18.09.2024 13:21

          И какие же?

          (А ещё я могу про сегментные регистры в ранних x86 рассказать. Вот там вообще ядрёная бомба будет.)


          1. perfect_genius
            18.09.2024 13:21
            +1

            Память комьютера — это огромная «шахматная доска»

            Т.е. чётные и нечётные элементы чем-то отличаются?

            в каждой клетке которой лежит число. В каких‑то из них могут лежать числа, которые нас по той или иной причине интересуют (например, потому что мы сами их туда положили), в остальных — лежат случайные числа («мусор»), но мы в них и не смотрим.

            Зачем нам класть числа в память? Откуда берётся мусор?

            Например, клетку № 153 422 731 мы отведём под число, которое будет счётчиком цикла.

            Почему в эту клетку, а не в первую? Что такое "счётчик цикла"?

            мы себе в табличку запишем, что i = 153422731 и будем везде использовать i.

            Почему "i"? Т.е. мы ограничены 26 буквами английского алфавита? Почему нельзя использовать мой родной язык?

            двоичный код

            мнемоническое «имя переменной»

            компиляции

            Всё, голова перегрелась от неизвестных непонятных слов.


            1. Wesha
              18.09.2024 13:21
              +8

              Т.е. чётные и нечётные элементы чем-то отличаются?

              Прикиньте себе, в некоторых компьютерахда. (Например, в PDP-11 MOV может работать только с чётными адресами, забирая оттуда 16 бит разом. А вот MOVB работает с байтами, но с любого адреса.)

              Зачем нам класть числа в память

              Вы ж программист. Хотите — кладите, не хотите — не кладите, но памяти совсем без чисел не бывает, там всегда что-то лежит, пусть даже и мусор.

              Откуда берётся мусор

              Остаточные заряды в конденсаторах. Или числа, оставшиеся от предыдущей программы. Тем и отличается malloc от calloc, что первой глубого пофиг, какой мусор лежит в выделенной ею области памяти, а вторая перед тем, как вернуть указатель, принудительно кладёт во все ячейки области по нулю. Потому-то она и называется callocclean & allocate

              Почему в эту клетку

              Во-первых, потому что это пример. Во-вторых, потому что так компилятор решил. А вот почему он так решил — это требует ответа ещё на пару страничек.

              а не в первую?

              Во-первых, в нулевую. Во-вторых, некоторые адреса выделены для особых нужд, и компилятор об этом знает.

              Что такое "счётчик цикла"?

              Эммммм... Вы точно программист?

              Т.е. мы ограничены 26 буквами английского алфавита?

              Эмммм... Да. И цифрами с подчёркиванием. Вы точно-точно программист?

              Почему нельзя использовать мой родной язык?

              Так исторически сложилось. Вы точно-точно-точно программист?

              голова перегрелась от неизвестных непонятных слов.

              Точно не программист.


              1. perfect_genius
                18.09.2024 13:21
                +1

                Вы почему-то забыли, что речь идёт о новичках, которым вы пытаетесь всё это объяснить.

                Только не показывайте это новичкам, иначе им взорвёт мозг.

                Такое бывает, когда отвечаешь разных собеседникам и теряешь детали диалога.


                1. Wesha
                  18.09.2024 13:21

                  Здесь новички не ходят. Если бы я писал книгу для новичков, все детали заняли бы страниц этак двадцать, а расписывать это бесплатно чисто для анонима на Хабре я не намерен.


  1. a1excoder
    18.09.2024 13:21
    +6

    очередная "hello world" на асме.. и ничего дальше


    1. redfox0
      18.09.2024 13:21
      +3

      Тут хотя бы современный 64-битный ассемблер, пример которого (упс!) соберётся и будет выполняться только в линуксе, так как используются системные вызовы, минуя, например, стандартную библиотеку Си.

      Чуть больше примеров:

      ; fasm hello.asm
      ; chmod +x hello
      ; ./hello
      format elf64 executable 3
      
      ; linux 2.6.35 compatible x86-64 syscalls table
      sys_read = 0
      sys_write = 1
      sys_exit = 60
      
      stdin = 0
      stdout = 1
      stderr = 2
      
      segment readable
      c_hello db "Hello world", 10
      .len = $ - c_hello
      
      segment executable
      entry $
      _start:
          ; write(stdout, c_hello, len)
          mov rdi, stdout
          mov rsi, c_hello
          mov rdx, c_hello.len
          mov rax, sys_write
          syscall
          
          ; exit(0)
          mov rdi, 0
          mov rax, sys_exit
          syscall
      

      Впрочем, интереснее вызывать уже готовые функции Си:

      ; fasm helloc.asm
      ; ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc helloc.o -o helloc
      format elf64
      extrn printf
      
          ; printf(c_hello, g_name)
          mov rdi, c_hello
          mov rsi, g_name
          call printf
      


  1. santanico
    18.09.2024 13:21
    +5

    Я бы не советовал использовать статьи для изучения чего-либо нового. Книга - таков путь!


    1. skymal4ik
      18.09.2024 13:21
      +2

      Для каких-нибудь ангуляров и реактов пока книга напечатается, она уже устареет. А по видеомонтажу в каком-нибудь блендере видеоуроки мне больше заходят, ибо нагляднее.

      Так что в ваших словах есть истина. Но не для всего.


  1. MountainGoat
    18.09.2024 13:21
    +4

    Лучше бы показали на практике, в чём смысл это учить - если не планируешь кодить на контроллерах с частотой 20Гц.


    1. Wesha
      18.09.2024 13:21

      В чём смысл это учить

      Ну да, ну да. Не нужно знать, как оно там унутрях устроено — надо чтобы нажал кнопку, и оно там само всё сделало! /s

      (Только учтите, что оно за Вас и конфеты есть будет.)


      1. Kirill-112
        18.09.2024 13:21

        (Только учтите, что оно за Вас и конфеты есть будет.)

        Представил нейросеть, которая ест конфеты быстрее и эффективнее, чем человек.


        1. Wesha
          18.09.2024 13:21
          +1

          Просыпайтесь, нейросеть уже жрёт электричество быстрее и эффективнее, чем человек.


          1. Ritan
            18.09.2024 13:21

            Как посчитать эффективность сжирания электричества? Чем ниже кпд тем эффективнее т.к. выше отношение сожранного к полезной работе?


            1. Wesha
              18.09.2024 13:21

              То есть вопроса "а как посчитать эффективность сжирания конфет" у Вас не возникает?


    1. LAutour
      18.09.2024 13:21

      Низкоуровневая отладка.


  1. ilshat1976
    18.09.2024 13:21
    +3

    Доброго времени суток!

    Что-то не понял насчёт вывода на экран, а где прерывание 21 или 9 (int 21h; int 09h)?

    Сейчас по другому можно выводить на экран? или я не те ассемблеры в ВУЗе на кафедре вычислительной техники изучал (в конце 90-ых), хотя IBMовский вроде тоже в их числе.


    1. horribile
      18.09.2024 13:21
      +3

      DOS и Linux немного отличаются...


    1. redfox0
      18.09.2024 13:21

      Раньше во времена 16 битного x86 действительно вызывали прерывания BIOS и (или) DOS.

      В статье показан 64 битный x86-64 и вызов системных вызовов ядра линукса 2.6.35+


  1. Fedorkov
    18.09.2024 13:21
    +2

    В качестве вводного гайда я бы привёл пример сишной программы с ассемблерной вставкой.

    Во‑первых, на чистом асме под линукс, кажется, пишут только демосценщики; а вставки используются гораздо чаще — в качестве микрооптимизации наиболее нагруженного кода.

    А во‑вторых, это будет более простой код, не привязанный к линуксу.

    Вот небольшой пример ассемблерной вставки у меня в гите: в файле knapsack.S реализована функция, переобъявленная с extern‑ом в main.c.


    1. Ritan
      18.09.2024 13:21

      не привязанный к линуксу

      Всё ещё привязан, просто не так явно. И может заработать на других платформах, где соглашение о вызовах совпадает с таковым в линуксе.


      1. Fedorkov
        18.09.2024 13:21

        Соглашение sysv_abi зафиксировано в объявлении в main.c.


        1. Ritan
          18.09.2024 13:21

          Которое, например, на windows не заработает. Или заработает с clang-cl, но так, что лучше бы не работало


  1. mezastel
    18.09.2024 13:21

    У меня на ассемблере написан только парсер интов. Потому что в разы быстрее чем то что дает любая библиотека. Использует, кстати, AVX.