Комментарии 41
Итак, теперь у нас есть все инструменты для создания ПО на ассемблере.
В этой первой статье мы изучили основные концепции ассемблера, попробовали на практике его синтаксис и даже написали работающее ПО.
Вот прямо вижу: "Я переписал весь код в Блокноте и сохранил в файл C:\asm\HelloWorld.asm - но не работает! даже не запускается...".
Если не считать этой досадной мелочи, а также того что напрямую к SDM не обратиться, ибо пошлёт, то всё остальное - ясно и понятно.
В ассемблере нет по умолчанию "невидимого" завершающего нуля, как Вы пишете. Так что на байт больше передали в системный вызов. У некоторых диалектов ассемблера есть .asciz директива, она добавляет 0 автоматически.
Системный вызов wtite не требует завершающего нуля, только количество байт для записа.
Там передали 14 байт, и в строке, вроде, 14 символов, с учётом '\n'.
Здесь нет ничего привычного нам: мы не видим ни условных операторов, ни циклов, нет никакого способа создавать функции…
В смысле, а на высокоуровневых языках вроде 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
, что первой глубого пофиг, какой мусор лежит в выделенной ею области памяти, а вторая перед тем, как вернуть указатель, принудительно кладёт во все ячейки области по нулю. Потому-то она и называется calloc
— clean & 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овский вроде тоже в их числе.
В качестве вводного гайда я бы привёл пример сишной программы с ассемблерной вставкой.
Во‑первых, на чистом асме под линукс, кажется, пишут только демосценщики; а вставки используются гораздо чаще — в качестве микрооптимизации наиболее нагруженного кода.
А во‑вторых, это будет более простой код, не привязанный к линуксу.
Вот небольшой пример ассемблерной вставки у меня в гите: в файле knapsack.S
реализована функция, переобъявленная с extern‑ом в main.c
.
У меня на ассемблере написан только парсер интов. Потому что в разы быстрее чем то что дает любая библиотека. Использует, кстати, AVX.
Вот это ужас-ужас, взорвёт мозг любому кто не в теме деталей:
Мы определили буфер в разделе .data ; (это `msg`), поэтому нужно просто скопировать его в `rsi`
К сожалению, такая же чушь в оригинале. Не "копирование буфера", потому что новичок поймёт это как копирование содержимого буфера (и будет удивляться, как оно влезло в регистр), а запись адреса буфера.
(Кстати, тут fasm, nasm и когда-то давно ещё TASM от Borland - установили правило, что аргумент указывает на содержимое в памяти только если [указатель в квадратных скобках], иначе это адрес места в памяти. Это жестковато, но правильнее. У MASM тут слегка каша.)
Ассемблер для программистов на языках высокого уровня: Hello World