Как-то раз я послушал следующее интересное выступление (по-немецки):
https://media.ccc.de/v/ds24-394-linux-hello-world-nur-mit-einem-hex-editor
В нём разобрано, как написать программу «hello world» для 64-разрядного дистрибутива Linux в шестнадцатеричном редакторе. Ассемблер здесь не используется, программа пишется непосредственно на машинном коде. Правда, в ней есть издержки на использование ELF.
Мне понравилась такая идея, и я решил повторить такой опыт, но немного в иной форме – а именно, под 16-разрядной DOS в реальном режиме. У меня должен был получиться файл в формате COM, а не EXE, так как (на данном этапе) меня интересовал не столько формат файла, сколько кодировка инструкций. В вышеупомянутой лекции, если честно, не сообщается почти никаких подробностей о том, как именно перейти от ассемблерного кода к машинному — поскольку в случае разбора этих тем лекция, пожалуй, растянулась бы на несколько часов. Но здесь я всё разберу подробно, и при этом собираюсь пользоваться только документацией lntel, а также дизассемблировать код в целях верификации.
Также мы коротко поговорим о сегментации.
В качестве шестнадцатеричного редактора на этот раз воспользуемся hexedit.
Как устроены файлы COM
«Программа» - это набор инструкций, записанных на диске. В нашем случае это инструкции x86. Чтобы эту программу можно было выполнить, её нужно загрузить в память, после чего можно будет перейти к тому адресу, по которому вы её загрузили (упрощённо говоря).
Среди множества различий между всевозможными форматами исполняемых файлов – как именно выполняется такая загрузка. При работе с файлами COM ваша программа будет загружаться со смещением 0100h от начала некоторого сегмента. (Говоря о шестнадцатеричных значениях, я буду пользоваться нотацией «0100h», а не «0x0100», так как в мире DOS более распространён именно первый вариант). Таким образом, именно по этому адресу будет (досимвольно) размещён весь ваш файл, независимо от его содержимого. Файлы COM под DOS не содержат ни заголовков, ни метаданных, ни чего-либо подобного. Далее файл начинает выполняться прямо с первого байта.
В этой программе мы не будем активно задействовать сегментацию, но понимать её основы необходимо. Как я сказал выше, у нас получится «16-разрядный код для DOS», а это значит, что регистры нашего ЦП будут, как правило, размером по 16 бит. Например, регистр AX имеет 16 разрядов в ширину. Так значит ли это, что мы можем адресовать в памяти всего 2^16 = 65536 байт? А вот и нет. В реальном режиме каждый адрес в памяти состоит из двух таких 16-разрядных значений. Это селектор сегмента и смещение от начала этого сегмента. Они хранятся в разных регистрах. Под селекторы сегментов отведён свой собственный набор регистров, эти регистры неприменимы для других целей. Например, в регистр DS («сегмент данных») может быть записано значение 0400h, а в регистре общего назначения DX может содержаться 20h. В результате получаем адрес 0400:0020.
Первым делом отметим здесь следующее: всё это совсем не означает, что мы, фактически, имеем дело с 32-разрядной адресацией. У нас в распоряжении всего 20 бит. Чтобы преобразовать два 16-разрядных значения в линейный (физический) 20-разрядный адрес, поступим так:
linear_address = (segment << 4) + offset
Этот слегка странный механизм означает, что несколько пар смещений и селекторов сегментов на практике отображаются на одни и те же линейные адреса.
При запуске программы в регистре CS записан какой-то селектор сегмента. DOS определит его значение за нас. Мы не будем знать этого значения до фактического запуска программы.
Теперь мы хотим вывести на экран строку «hello world». То есть, эту строку требуется каким-то образом включить в наш файл. После того как DOS загрузит наш файл COM в память, можно будет определить тот адрес, с которого начинается строка: это 0100h плюс смещение строки в данном файле. Таким образом, если «h» записано в 80-м байте файла (80 = 50h), то строка начнётся с CS:0150, где «CS» - это значение, записанное в данный момент в регистре CS.
Версия на псевдокоде
Примерно, как и в той лекции, на которую я сослался в начале, наша программа должна будет выполнять два шага:
1. Выводить строку на экран.
2. Осуществлять выход.
За обе операции будут отвечать системные вызовы DOS (есть и другие варианты, которые было бы очень интересно исследовать, но я не хочу чрезмерно усложнять эту статью). Итак, первым делом давайте разберёмся, как работает API системных вызовов в DOS. В наше время эту информацию довольно легко нагуглить. Проще пареной репы. Например, ниже приведена ссылка на «веб-клон» программы HelpPC:
Вот список сервисов DOS:
https://helppc.netcore2k.net/interrupt/dos-services
Нотация INT 21,1B означает, что требуется выдать программное прерывание номер 21 (шестнадцатеричное), а до того в некоторый регистр должно быть записано значение 1B (шестнадцатеричное). Такой способ вызова функций операционной системы оставался неизменным до тех пор, пока, наконец, в Pentium II не появилась специальная инструкция SYSENTER (или когда это на самом деле было?). Примерно такова же ситуация под 32-разрядным Linux: вы записываете номер нужного вам системного вызова в регистр EAX, после чего выдаёте прерывание 80h.
Итак, какими функциями можно было бы воспользоваться? Во-первых, есть INT 21,9:
https://helppc.netcore2k.net/interrupt/int-21-9
Она выводит на экран строку, в точности как мы и хотели.
Также мы должны явно приказать DOS завершить нашу программу. Это необходимо, так как ЦП начинает выполнять программу с первого байта/первой инструкции. Затем он читает и выполняет следующую, после этого следующую и так далее. Конца этому нет. Если бы мы не выдали команду «на выход», то процессор так и продолжал бы выполнять произвольное содержимое из памяти, т.е. мусор. Скорее всего, из-за этого программа бы аварийно завершилась.
Чтобы завершить программу правильно, воспользуемся INT 21,4C:
https://helppc.netcore2k.net/interrupt/int-21-4c
Итак, давайте подробнее рассмотрим INT 21,9 — как она выводит на экран строку. Описание очень краткое. Она начинается с AH = 09, и это означает, что мы должны записать шестнадцатеричное значение 9 в регистр AH. Строго говоря, AH сам по себе — это не настоящий регистр, а просто верхние 16 разрядов регистра AX. В свою очередь, AL — это нижние 16 разрядов AX.
Затем читаем:
DS:DX = указатель на строку, которая оканчивается "$"
Итак, здесь у нас есть адрес в памяти, то есть, мы имеем дело с сегментацией. В регистре DS будет находиться селектор сегмента, а в регистре DX — смещение.
Каков адрес нашей строки? Напомню: нам известно, что весь файл был загружен по адресу CS:0100, так что для начала нужно скопировать значение CS в DS. В DX должно быть 0100 + смещение до строки, записанной в нашем файле.
На самом деле, я лукавлю. Мы не обязаны копировать CS в DS – на момент запуска нашей программы значения в обоих этих регистрах будут одинаковы. Так или иначе, мы всё равно включим операцию копирования, поскольку впоследствии (когда зайдёт речь о кодировке инструкций) нам придётся иметь дело с чуть более сложным случаем.
После того, как будут заданы регистры AH, DS и DX, инициируем прерывание 21h.
Вызов на выход из INT 21,4C работает аналогично. В нём сказано:
AH = 4C
AL = код возврата (для пакетных файлов)
Воспользуемся кодом возврата 42 (в шестнадцатеричной системе — 2A).
Вот как выглядит наша программа на псевдокоде:
set AH to 09h
copy CS to DS
set DX to $string_offset
trigger interrupt 21h
set AH to 4Ch
set AL to 2Ah
trigger interrupt 21h
(Я специально не стал писать это на ассемблере, чтобы мы не могли «сжульничать». :-)).
Шаг 1: вызов на выход
Сначала напишем код для вызова, обеспечивающего выход, а позже — для вызова, выводящего текст на экран. Именно таким способом проще приступить к работе.
Первым делом отправляемся на сайт Intel и скачиваем документацию:
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
Рекомендую скачивать тома как отдельные файлы («Ten-Volume Set of Intel 64 and IA-32 Architectures Software Developer's Manuals»), поскольку в полной версии документации более 5000 страниц, и сориентироваться в ней непросто.
Ладно, так как установить в регистре AH значение 4Ch? Для копирования данных предназначена инструкция MOV. Открываем эту часть мануала (том 2B).
Откуда вообще берётся значение 4Ch? Должны ли мы первым делом загрузить его в память? Если так, то как адресовать эту память? Я имею в виду, что 4Ch определённо будет входить в состав нашего файла, поэтому… может быть… воспользуемся смещением, чтобы найти 4Ch у нас в файле? Откуда тогда нам взять значение этого смещения? Напоминает парадокс курицы и яйца.
В данном случае нам потребуется «непосредственное значение»: такое, которое встраивается прямо в код (поэтому, да, неявно оно всё должно быть загружено в память, но DOS делает это за нас.)
Заглянув в документацию, находим следующее:
MOV -- Move
Opcode | Instruction | Op/En | ... | Description
----------+--------------+-------+-----+----------------
B0+ rb ib | MOV r8, imm8 | OI | ... | Move imm8 to r8.
«imm8» — это непосредственное значение размером 8 бит. «r8» — это регистр шириной 8 бит. В ассемблере можно было бы просто написать вот так:
MOV AH, 4Ch
Нам более интересен столбец с кодом операции (опкодом). Код операции — это байт на диске, относящийся к вашей программе, и опкод для данного варианта инструкции MOV — это «B0+». Правда, видим, что это не совсем байт, что здесь означает знак «+»? Об этом рассказано в разделе 3.1.1.1 тома 2A. Вообще, регистры нумеруются, и вы просто добавляете к B0 номер желаемого регистра. В таблице 3-1 из тома 2A перечислены все номера, релевантные в данном контексте.
Мы хотим скопировать данные в регистр AH, номер которого — 4, так что получается B4.
Байт, следующий прямо за ним — это и есть непосредственное значение, в нашем случае это просто 4Ch.
К настоящему моменту наш файл выглядит так:
B4 4C
Далее мы хотим скопировать 2Ah в регистр AL. У AL номер 0, следовательно, получаем:
B4 4C B0 2A
Наконец, нам нужно инициировать прерывание. Это делается при помощи инструкции INT (в документации она описана в томе 2A). По описанию она немного проще MOV:
INT n -- Call to Interrupt Procedure
Opcode | Instruction | Op/En | ... | Description
-------+-------------+-------+-----+----------------
CD ib | INT imm8 | I | ... | Generate software interrupt
with vector specified by
immediate byte.
Здесь «CD» — это просто «CD». В следующем байте номер прерывания записан как непосредственное значение.
Теперь в нашем файле содержатся эти необработанные байты (пробелы вставлены только для удобочитаемости):
B4 4C B0 2A CD 21
Назовём его hello.com.
В качестве первого теста воспользуемся objdump под Linux и попробуем дизассемблировать этот код:
$ objdump -D -b binary -mi386 -Maddr16,data16,intel hello.com
hello.com: file format binary
Disassembly of section .data:
00000000 <.data>:
0: b4 4C mov ah,0x4c
2: b0 2a mov al,0x2a
4: cd 21 int 0x21
Неплохо!
Давайте запустим его в DOS. Для этого я воспользуюсь FreeDOS 1.3 под виртуальной машиной QEMU, поскольку это свободный софт, вдобавок оснащённый отладчиком, работающим в пользовательском пространстве. Этим он отличается от DOSBox, требующим активировать отладчик во время компиляции. Думаю, просто невозможно запретить ему показывать такие внутренние элементы как тактовые прерывания, которые здесь совершенно неуместны.

На первый взгляд, ничего не произошло, но что я хочу отметить: здесь не было ни каких-либо ошибок, ни неожиданного поведения.
Так что давайте запустим отладчик и изучим, что происходит:

Расшифровка:
C:\>debug a:\hello.com
-U CS:0100 L 6
088D:0100 B44C MOV AH,4C
088D:0102 B02A MOV AL,2A
088D:0104 CD21 INT 21
-R
AX=0000 BX=0000 CX=0006 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=088D ES=088D SS=088D CS=088D IP=0100 NV UP EI PL ZR NA PE NC
088D:0100 B44C MOV AH,4C
-T
AX=4C00 BX=0000 CX=0006 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=088D ES=088D SS=088D CS=088D IP=0102 NV UP EI PL ZR NA PE NC
088D:0102 B02A MOV AL,2A
-T
AX=4C2A BX=0000 CX=0006 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=088D ES=088D SS=088D CS=088D IP=0104 NV UP EI PL ZR NA PE NC
088D:0104 CD21 INT 21
-T
Program terminated normally (002A)
-
Символ – здесь означает приглашение командной строки.
Команда U дизассемблирует наш код (в данном случае U – это сокращение от «unassemble»). Я скомандовал начать работу с адреса CS:0100 длиной 6 байт. Видим тот же самый ассемблерный код, что и в случае с objdump.
Далее команда R отображает, что в настоящий момент содержится в регистрах. Обратите внимание: AX=0000, так что пока регистр AX целиком заполнен нулями. Также обратите внимание на значения регистров CS и IP: CS, регистр с сегментом кода, содержит значение 088D, это и есть наш актуальный сегмент (ещё вы увидите 088D в выводе команды U). IP, указатель инструкций, направлен на следующую инструкцию, которую нужно выполнить — он также показан ниже в списке регистров. Итак, вот наша следующая инструкция:
MOV AH, 4Ch
Затем я трижды выдал команду T. Обратите внимание, как три раза меняется значение AX: сначала верхняя часть устанавливается в 4C, потом нижняя часть в 2A. Наконец, выдаём прерывание 21h, на что debug отвечает: «Program terminated normally (002A)» (программа завершилась нормально).
Выглядит хорошо. Именно этого мы и хотели добиться.
Шаг 2: вызов для вывода на экран
Вспомните, что мы собирались делать:
set AH to 09h
copy CS to DS
set DS to $string_offset
trigger interrupt 21h
Мы уже знаем, как закодировать MOV AH, 09h:
B4 09
Далее скопируем CS в DS. Как было показано выше в отладочном выводе, особой необходимости в этом нет, но операция, тем не менее, интересная.
Здесь нам не подойдёт инструкция MOV, начинающаяся с B0, так как она предназначена лишь для копирования непосредственных значений в регистр. Потребуется воспользоваться другим вариантом MOV:
MOV -- Move
Opcode | Instruction | Op/En | ... | Description
-------+-----------------+-------+-----+----------------
8C /r | MOV r/m16, Sreg | MR | ... | Move segment register to
r/m16.
На этот раз в столбце «Op/En» фигурирует «MR», и мы вступаем в довольно запутанный и гнусный мир байт ModR/M. Если в нём разобраться, то не так уж он и сложен, но мне пришлось поломать голову, чтобы всё встало на свои места. Intel пытается объяснить эти вещи в разделе 2.1.5 (том 2A) и в приложении B в конце тома 2D.
В общем виде байт ModR/M выглядит так:
[ . . | . . . | . . . ]
MOD REG R/M
В двух крайних левых битах указывается значение поля «MOD», следующие три бита отводятся под «REG», а ещё три — под «R/M». Этот байт нужен, чтобы закодировать два операнда и, как будто этого мало — режим адресации: ссылается ли R/M непосредственно на регистры или опосредованно на память, адресованную содержимым регистров?
В списке регистров выше видим «8C /r». Вот что об этом написано в разделе 3.1.1.1 (том 2A):
/r – указывает, что в байте ModR/M этой инструкции содержится операнд регистра и операнд r/m.
Как сказано в разделе 2.1.5, эта MOD должна быть равна 11 (в двоичной системе), если вы хотите указать регистр, так что это именно наш случай. Далее в разделе 2.1.5 разобрано несколько таблиц, в которых перечислено несколько комбинаций MOD и R/M, но ... сегментных регистров здесь нигде не видно. То есть, это нам пока не помогает, нужно больше подробностей.
Обратимся к приложению B в конце тома 2D. Начнём с B-13. Где-то в разделе «Move to/from Segment Registers» описана наша инструкция MOV. Вот какие строки из этой таблицы нас интересуют:
Instruction and Format | Encoding
--------------------------------+-------------------------
MOV (to/from segment registers) |
register to segment register | 1000 1110 : 11 sreg3 reg
segment register to register | 1000 1100 : 11 sreg3 reg
Хорошо, 10001100b = 8Ch. Затем видим, что MOD действительно должна быть равна 11b. Часть REG в байте ModR/M — это «sreg3», а часть R/M — это «reg».
По поводу «sreg3» заглянем в таблицу B-8.
Информация о «reg» содержится в таблице B-4. Почему именно в B-4? Так как «поле w» не входит в состав нашей инструкции. Если снова прокрутите B-13, то увидите, что иногда «w» фигурирует вместо литерального бита. Это заполнитель, по которому определяется размер операндов. Для работы с сегментными регистрами он не нужен, поскольку нам уже известно, что их размер всегда составляет 16 бит.
Хорошо, значит, теперь мы знаем, какие числа следует записать в байт ModR/M, чтобы указать желаемые регистры.
Обратите внимание: байт ModR/M не симметричен. REG не может ссылаться на память, только R/M может.
Наша инструкция 8C обозначена как «MR», а ниже перечислены различные варианты MOV. Есть и другая таблица, в которой сказано, что в данном случае означают «MR» и «RM»:
Op/En | Operand 1 | Operand 2
------+-----------+----------
MR | ModRM:r/m | ModRM:reg
RM | ModRM:reg | ModRM:r/m
Итак ... 8C может скопировать значение сегментного регистра в другой регистр или в память. Её первый операнд — «r/m16». Этот первый операнд закодирован в части R/M байта ModR/M. Второй операнд — это «Sreg», сегментный регистр, и он находится в части REG.
Рассмотрим пример. Хотим закодировать это:
MOV AX, DS
Код операции 8C, и в байте ModR/M обязательно должны быть эти значения (DS берётся из таблицы B-8, а AX — из B-4):
[ 1 1 | 0 1 1 | 0 0 0 ] = D8h
MOD REG R/M
DS AX
Запишем два этих байта в файл и посмотрим, что по этому поводу скажет objdump:
$ xxd ex1.com
00000000: 8cd8 ..
$ objdump -D -b binary -mi386 -Maddr16,data16,intel ex1.com
ex1.com: file format binary
Disassembly of section .data:
00000000 <.data>:
0: 8c d8 mov ax,ds
Хорошо.
А что нам помешает поступить так, как показано ниже? Ведь именно в этом и заключается наша цель:
MOV DS, CS
Здесь должен быть этот байт ModR/M, верно?
[ 1 1 | 0 0 1 | 0 1 1 ] = CBh
MOD REG R/M
CS DS?
К сожалению, дизассемблировав этот код, получим:
00000000 <.data>:
0: 8c cb mov bx,cs
Оказывается, сделать так нельзя, а почему — описано в таблице B-13, где допускается всего один компонент sreg3 и один компонент reg, но не два компонента sreg3.
Ещё улавливаете нить сюжета? Уф.
Что нам придётся сделать: скопировать CS в регистр общего назначения, а затем в другой сегментный регистр (DS).
Ладно. Вполне можно скопировать CS куда-нибудь ещё, например, в CX:
MOV CX, CS --> Opcode 8C
[ 1 1 | 0 0 1 | 0 0 1 ] = C9h
MOD REG R/M
CS CX
При работе с MOV DS, CX потребуется использовать код операции 8E, но, к счастью, он функционально аналогичен 8C, по сравнению с байтом ModR/M лишь операнды меняются местами (он фигурирует как «RM», а не как «MR»). Поэтому:
MOV DS, CX --> Opcode 8E
[ 1 1 | 0 1 1 | 0 0 1 ] = D9h
MOD REG R/M
DS CX
Изрядно помучившись, вспомним, какой вид к настоящему моменту приобрела эта часть программы:
B4 09 8C C9 8E D9
В дизассемблированном виде:
00000000 <.data>:
0: b4 09 mov ah,0x9
2: 8c c9 mov cx,cs
4: 8e d9 mov ds,cx
Далее нужно установить в качестве значения DX требуемое смещение. Оказывается, это не так просто, поскольку мы пока не дописали нашу программу, и поэтому точного значения смещения не знаем. Пока применим формальное смещение 1234h, а позже подставим на его место верное значение.
Для установки DX можно вновь воспользоваться сравнительно простым кодом операции, не требующим задействовать ещё один байт ModR/M:
MOV -- Move
Opcode | Instruction | Op/En | ... | Description
----------+----------------+-------+-----+----------------
B8+ rw iw | MOV r16, imm16 | OI | ... | Move imm16 to r16.
Таким образом, он аналогичен рассмотренному выше B0, но состоит из 16 разрядов. Номер регистра DX — 010 (в двоичном формате): B8 + 02 = BA.
Поскольку в x86 используется нумерация байт от младшего к старшему, в итоге имеем:
BA 34 12
Причём, мы уже знаем, какие нужны значения, чтобы инициировать прерывание 21h:
CD 21
Шаг 3: добавляем строку и завершаем программу
Итак. Давайте посмотрим, как сейчас выглядит вся наша программа:
B4 09 8C C9 8E D9 BA 34 12 CD 21 ; print
B4 4C B0 2A CD 21 ; exit
В дизассемблированном виде:
00000000 <.data>:
0: b4 09 mov ah,0x9
2: 8c c9 mov cx,cs
4: 8e d9 mov ds,cx
6: ba 34 12 mov dx,0x1234
9: cd 21 int 0x21
b: b4 4c mov ah,0x4c
d: b0 2a mov al,0x2a
f: cd 21 int 0x21
Нам по-прежнему ещё нужно сохранить строку «hello world» и исправить смещение.
Добавить строку — тривиальная задача. Просто ставите её куда нужно. В конце. Сразу после CD 21. Вот шестнадцатеричный дамп:
$ xxd hello.com
00000000: b409 8cc9 8ed9 ba34 12cd 21b4 4cb0 2acd ..........!.L.*.
00000010: 2148 656c 6c6f 2c20 776f 726c 6421 0d0a !Hello, world!..
00000020: 24 $
А что здесь означает знак доллара? Вернёмся к описанию INT 21,9:
https://helppc.netcore2k.net/interrupt/int-21-9
По причинам, которые, пожалуй, заслуживают отдельной статьи, строки в такой функции требуется завершать знаком доллара.
Теперь можно исправить смещение. В шестнадцатеричном редакторе видим, что символ «H» в «Hello, world!» в файле стоит со смещением 11h. Таким образом, необходимо установить DX в значение 111h в качестве смещения в памяти (напомню, весь наш файл загружается со смещением 0100h).
Заменим
BA 34 12
на
BA 11 01
— и всё готово
Вот вся программа целиком:
$ xxd hello.com
00000000: b409 8cc9 8ed9 ba11 01cd 21b4 4cb0 2acd ..........!.L.*.
00000010: 2148 656c 6c6f 2c20 776f 726c 6421 0d0a !Hello, world!..
00000020: 24 $
$ objdump -D -b binary -mi386 -Maddr16,data16,intel --stop-address=17 hello.com
hello.com: file format binary
Disassembly of section .data:
00000000 <.data>:
0: b4 09 mov ah,0x9
2: 8c c9 mov cx,cs
4: 8e d9 mov ds,cx
6: ba 11 01 mov dx,0x111
9: cd 21 int 0x21
b: b4 4c mov ah,0x4c
d: b0 2a mov al,0x2a
f: cd 21 int 0x21
(Здесь используем --stop-address=17, поскольку следующие далее байты — это наша строка. Это данные, а не код.)
Действительно, всё работает:

Бонус: префикс переопределения размера операндов
операндов
В этой лекции B8 01 00 00 00 используется на месте MOV EAX, 1. Как такое возможно? Разве не при помощи B8 мы записали 16 бит в AX?
Разница в том, что наш процесс работает в 16-битном режиме, а процесс, описанный в той лекции — нет.
Раздел 2.1.1 (том 2A) знакомит нас с префиксами инструкций:
Префикс переопределения размера операндов позволяет переключать программу между 16- и 32-разрядными операндами. Любой из этих размеров можно задать по умолчанию, а при использовании префикса используется тот размер, что не задан по умолчанию.
Чуть выше сообщается, что значение этого префикса — 66h.
Итак, если бы мы захотели записать информацию в EAX, а не в AX, то должны были бы сделать так:
$ xxd 32.com
00000000: 66b8 4433 2211 f.D3".
$ objdump -D -b binary -mi386 -Maddr16,data16,intel 32.com
32.com: file format binary
Disassembly of section .data:
00000000 <.data>:
0: 66 b8 44 33 22 11 mov eax,0x11223344
Чтобы выполнить эту программу, понадобится версия 386 или младше. Ходят слухи, что это работает даже в реальном режиме, но я не углублялся в исследование этого вопроса.
Соответственно, 66h переключается на 16-разрядные размеры не-16-разрядных режимах:
$ cat linux-exit.s
.text
.global _start
_start:
movw $0x1234, %ax
/* _exit(0); */
movl $1, %eax
movl $0, %ebx
int $0x80
$ as -g -o linux-exit.o linux-exit.s && ld -o linux-exit linux-exit.o
$ file linux-exit
linux-exit: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
$ objdump -d linux-exit
linux-exit: file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <_start>:
401000: 66 b8 34 12 mov $0x1234,%ax
401004: b8 01 00 00 00 mov $0x1,%eax
401009: bb 00 00 00 00 mov $0x0,%ebx
40100e: cd 80 int $0x80
Комментарии (9)
apevzner
04.08.2025 22:12b: b4 4c mov ah,0x4c
d: b0 2a mov al,0x2a
Хм. Можно было бы сэкономить один байт, сказав
mov ax, ...
. А можно было бы иint 20h
сказать, без mov-ов...NeriaLab
04.08.2025 22:12mov ax, 4C2Ah ; AH = 4Ch, AL = 2Ah — то же самое, что два mov int 21h
Тогда почему часто делают два mov?
читаемость и семантика;
гибкость при изменении кода возврата;
оптимизация компилятора не всегда выбирает mov ax, imm16;
психология и традиция
Пример гибкости кода возврата:
mov ah, 4Ch ; функция exit — фиксирована mov al, [result] ; код возврата — переменный int 21h
Пример кода на C:
exit(42);
Вы же можете изменить код возврата, как пример - согласно условиям?!
NeriaLab
04.08.2025 22:12int 20 - завершение программы (Terminate). Работает только для .COM-файлов - не подходит для .EXE Требует, чтобы CS указывал на начало программы, если вы изменили CS, то он не сработает. Не позволяет передать код возврата.
int 21 - главный системный вызов DOS, через который можно делать разные операции: вывод на экран, чтение с клавиатуры, работу с файлами, управление памятью, завершение программы с кодом возврата.
Zara6502
04.08.2025 22:12не совсем понял смысла челенжа, мнемокод и номер команды неразрывно связаны, пишете сначала на ассемблере, потом конвертируете в HEX, создаёте в HEX редакторе - вуаля. Но это не имеет ни практического ни развлекательного смысла.
skymal4ik
Было интересно, спасибо. Понастольгировал по ассемблеру и com файлах.
PS скриншоты такого качества, что ничего не видно.
hurtavy
Ага, олдскулы сводит, когда читаешь, как кто-то открывает для себя то, с чего ты начинал много лет назад
horribile
Я, вот, начинал с ассемблера PDP-11, тоже было интересно :)
Rigidus
А статей нет - непорядок...