Как стать автором
Обновить

Ассемблер для программистов на языках высокого уровня: Hello World

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров23K
Всего голосов 61: ↑57 и ↓4+69
Комментарии41

Комментарии 41

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

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

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

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

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

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

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

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

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

13 надо указать.

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

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

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

А msg?

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

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

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

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

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

(format t "Hello, World!")

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

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

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

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

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

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

И какие же?

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

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

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

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

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

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

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

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

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

двоичный код

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

компиляции

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

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

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

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

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

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

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

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

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

а не в первую?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Тут хотя бы современный 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

Вот ещё "Hello World!" на 4-х ассемблерах. )))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вот это ужас-ужас, взорвёт мозг любому кто не в теме деталей:

Мы определили буфер в разделе .data ; (это `msg`), поэтому нужно просто скопировать его в `rsi`

К сожалению, такая же чушь в оригинале. Не "копирование буфера", потому что новичок поймёт это как копирование содержимого буфера (и будет удивляться, как оно влезло в регистр), а запись адреса буфера.

(Кстати, тут fasm, nasm и когда-то давно ещё TASM от Borland - установили правило, что аргумент указывает на содержимое в памяти только если [указатель в квадратных скобках], иначе это адрес места в памяти. Это жестковато, но правильнее. У MASM тут слегка каша.)

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий