Показаны сообщения с ярлыком практика программирования. Показать все сообщения
Показаны сообщения с ярлыком практика программирования. Показать все сообщения

пятница, 29 мая 2009 г.

Опять играем со стеком

По ходу экспериментов над нашим мутантом, пришла в голову мысль создать еще один штамм мутантика. Идея состояла в следующем: изменить непосредственно в стеке адрес возврата из подпрограммы, так чтобы подпрограмма вернула управление не на следующую команду после вызвавшей ее команды call, а на команду, указанную нами. Для этого, конечно, пришлось немного забежать вперед по тексту учебника, чтобы найти подходящие для реализации этого финта ушами команды (я так подозреваю, что не самые оптимальные, но замысел удался! :) Вот наш новый мутантик:

Теперь смотрим листинг:

Погружаемся в медитацию...
Команда call subp теперь имеет код E8FFE4. Складываем FFE4h+1Ch получаем 0 (проверьте в калькуляторе Windows предварительно выставив длину 2 байта). Команда jmp finish имеет вид EBF7, где собственно F7 отрицательное (в данном случае) смещение равное -9. То есть эта команда вычитает 9 из следующего после нее адреса (вернее она складывает шестнадцатеричные значения). Проверяем 25h+F7=1C (проверьте в калькуляторе Windows, предварительно выставив длину 1 байт). Почему выставляем длину, то один, то два байта – это тема для медитации :)
Теперь посмотрим прогу в hiew...

Внимательно медитируем на стрелочки :) И далее погружаемся в глубокую медитацию на прогу в отладчике...

Скриншот сделан после выполнения команды call subp (call 0000). Обращаем внимание, что адрес возврата из подпрограммы помещен в стек и равен 001C, как и должно быть. Дебагим нашу прогу дальше до команды ret (ее не выполняем):

Как видим, наш код поменял непосредственно в стеке адрес возврата из подпрограммы, поэтому команда ret извлечет из стека уже не адрес 001С, а адрес 0021, передав таким образом управление уже на этот адрес.

Ну и приведем вывод нашей программы:

C:\ASM\HELLO\TASM>hello10
-=* Hello World *=--=* Hello World *=--=* Hello World *=-
C:\ASM\HELLO\TASM>

четверг, 28 мая 2009 г.

Игры со стеком

Еще немного поиграем со стеком и подпрограммами, чтобы лучше усвоить как работает стек и передается управление на подпрограммы. Для этого выведем новый штамм нашей мега программы:

Как видно из скрина, мы переместили подпрограмму в начало сегмента кода. Посмотрим теперь на листинг:

Как видим, машинный код команды call subp изменился, теперь он имеет вид - E8FFEE, где FFEE является смещением на начало подпрограммы. Складываем адрес следующей команды, после команды call с этим смещением 12h+FFEEh=0. Можете проверить это в калькуляторе Windows, только не забудьте выставить длину в ДВА БАЙТА (Почему именно в два байта? Ну помедитируйте на это :) ). Здесь мы опять встретилсь с явлением циклического возврата или оборачивания адреса. Помедитируем на это немного и дальше в путь... (Подсказка для медитирующих: 12h=18d и это как раз количество команд отделяющих адрсе 12h до начала нашей подпрограммы. Вобщем медитируйте...)
Далее на нашем пути hiew...

Hiew, надо отметить, преполезнейшая вещь.
Ну и теперь отладчик :)

Этот скрин сделан еще до выполнения первой команды. Обращаем внимание на точку входа в программу и на состояние регистров SS и SP. Следующий скрин сделан уже после входа в подпрограмму, но еще до выполнения первой ее команды. Смотрим, медитируем, просветляемся...

среда, 27 мая 2009 г.

Стек и подпрограммы

В своем изучении ассемблера, я немного отклонился от линии начертанной великим Питерем Аблем, но я думаю он на меня не обидется :) Еще немного поэкспериментируем со стеком и подпрограммами. Немного практики ни когда не повредит, а потом вернемся к генеральной линии партии... ммм.... вернее Питера.... Абеля....

Итак, наша програ продолжает мутировать:

Оператор CALL вызывает подпрограмму subp, которую мы определили с помощью соответствующих директив PROC и ENDP. Оператор RET осуществляет возврат из подпрограммы в головную программу.
Взглянем на листинг:

Внимательно медитируем на листинг...
Команда call subp имеет машинный код E80005, где собственно E8 – это сама команда, а 0005 – это смещение, указывающее на первую команду нашей подпрограммы. Команда call уменьшает значение SP на два, помещает в стек адрес, следующей за ней команды (в нашем случае адрес команды mov ax,4c00h равный 000F). Затем вычисляет адрес команды подпрограммы, на которую надо передать управление, прибавляя к адресу следующей за ней команды смещение (положительное или отрицательное) равное количеству байт программы, до первой команды вызываемой подпрограммы – в нашем случае 5 байт (000F+5=0014) и помещает это число (в нашем случае 0014) в регистр IP, передавая таким образом управление на подпрограмму. Подпрограмма выполняется до команды RET, которая извлекает из стека адрес возврата из подпрограммы (в нашем случае 000F) и помещает его в регистр IP (передавая, таким образом, управление вызвавшей ее программе) и уменьшает значение SP на два.
И, традиционно, помедитируем на код сгенерированный ассемблером:

Посмотреть на программу в hiew, так же не повредит:

Ну и, святое дело, дебагер:

Скриншот сделан еще до выполнения первой команды. Внимательно медитируем и жмем пять раз F8, то есть останавливаемся на команде call (ее не выполняем).

Из скриншота видно, что отладчик не плохо поорудовал в стеке, но при этом значение регистра SP осталось прежним, и он по прежнему указывает на дно стека (вернее на первое слово под ним). Делаем выводы и далее жмем один раз F7, чтобы войти в подпрограмму.

Как, уже говорилось, команда call уменьшила содержимое регистра SP на два (0040-2=003E), поместила в стек адрес следующей за ней команды (000F), прибавила к адресу следующей за ней команды смещение до первой исполняемой команды подпрограммы (000F+5=14h) и поместила это значение (14h) в регистр IP.
Далее жмем два раза F8.

Команда ret, извлекла из стека адрес возврата из подпрограммы (000F) и поместила его в регистр IP, а затем увеличила значение регистра SP на два, вернув указатель стека, опять на его дно.
Ну и покажем вывод нашей мега программы. Она два раза выводит знаменитую фразочку, но без перевода каретки на новую строку (так как мы это не запрограммировали.

C:\ASM\HELLO\TASM>hello8
-=* Hello World *=--=* Hello World *=-
C:\ASM\HELLO\TASM>

воскресенье, 24 мая 2009 г.

И снова "Здравствуй МИР!"

И так, подведем небольшой итог всему что было до этого. Так как, особо, не объяснялось что делает каждая команда великой и могучей программы "Hello World", то тут этот досадный недостаток будет полностью устранен. Еще раз взглянем на самую первую, не претерпевшую ни каких мутаций, программу:

Как уже не раз говорилось, обращение к памяти осуществляется исключительно посредством сегментов - логических образований, накладываемых на любые участки физического адресного пространства. Начальный адрес сегмента, деленный на 16, т.е. без младшей шестнадцатеричной цифры, заносится в один из сегментных регистров; после этого мы получаем доступ к участку памяти, начинающегося с заданного сегментного адреса.
Каким образом понятие сегментов памяти отражается на структуре программы? Следует заметить, что структура программы определяется, с одной стороны, архитектурой процессора (если обращение к памяти возможно только с помощью сегментов, то и программа, видимо, должна состоять из сегментов), а с другой - особенностями той операционной системы, под управлением которой эта программа будет выполняться. Наконец, на структуру программы влияют также и правила работы выбранного транслятора - разные трансляторы (например MASM и TASM в режиме IDEAL) предъявляют несколько различающиеся требования к исходному тексту программы (хотя TASM по умолчанию работает в режиме совместимости с MASM ).
Следует заметить, что при вводе исходного текста программы с клавиатуры можно использовать как прописные, так и строчные буквы; транслятор воспринимает, например, строки mov ax,data и mov ax.data одинаково. Однако с помощью соответствующих ключей можно заставить транслятор различать прописные и строчные буквы в отдельных элементах предложений. Предложения языка ассемблера могут содержать комментарии, которые отделяются от предложения языка знаком точки с запятой (;). при необходимости комментарий может занимать целую строку (тоже, естественно, начинающуюся со знака ";"). Поскольку в языке ассемблера нет знака завершения комментария, комментарий нельзя вставлять внутрь предложения языка, как это допустимо делать во многих языках высокого уровня. Каждое предложение языка ассемблера, даже самое короткое, должно занимать отдельную строку текста.
В нашей программе описаны три сегмента: сегмент команд с именем code, сегмент данных с именем data и сегмент стека с именем stk. Описание каждого сегмента начинается с ключевого слова segment, предваряемого некоторым именем, и заканчивается ключевым словом ends, перед которым указывается то же имя, чтобы транслятор знал, какой именно сегмент мы хотим закончить. Имена сегментов выбираются вполне произвольно. текст программы заканчивается директивой ассемблера end, завершающей трансляцию. В качества операнда этой директивы указывается точка входа в программу; в нашем случае это метка begin.
Порядок описания сегментов в программе, как правило, не имеет значения. Часто программу начинают с сегмента данных, это несколько облегчает чтение программы, и в некоторых случаях устраняет возможные неоднозначности в интерпретации команд, ссылающиеся на данные, которые еще не описаны. Мы в начале программы расположили сегмент команд, за ним - сегмент данных и в конце - сегмент стека; такой порядок предоставляет некоторые удобства при отладке программы. Важно только понимать, что в оперативную память компьютера сегменты попадут в том же порядке, в каком они описаны в программе (если специальными средствами ассемблера не задать иной порядок загрузки сегментов в память).
Сегменты вводятся в программу с помощью директив ассемблера segment и ends. Что такое директива ассемблера? В тексте программы встречаются ключевые слова двух типов: команды процессора (mov, int) и директивы транслятора (в данном случае термины "транслятор" и "ассемблер" являются синонимами, обозначая программу, преобразующую исходный текст, написанный на языке ассемблера, в коды, которые будут при выполнении программы восприниматься процессором). К директивам ассемблера относятся обозначения начала и конца сегментов segment и ends; ключевые слова, описывающие тип используемых данных (db, dup); специальные описатели сегментов вроде stack и т. д. Директивы служат для передачи транслятору служебной информации, которой он пользуется в процессе трансляции программы. Однако в состав выполнимой программы, состоящей из машинных кодов, эти строки не попадут, так как процессору, выполняющему программу, они не нужны. Другими словами, операторы типа segment и ends не транслируются в машинные коды, а используются лишь самим ассемблером на этапе трансляции программы.
Еще одна директива ассемблера используется в первом предложении программы:

assume cs:code,ds:data

здесь устанавливается соответствие сегмента code сегментному регистру cs и сегмента data сегментному регистру ds. Первое объявление говорит о том, что сегмент code является сегментом команд, и встречающиеся в этом сегменте метки принадлежат именно этому сегменту, что помогает ассемблеру правильно транслировать команды переходов (при использовании транслятора masm эта часть объявления необходима в любой, даже самой простой программе).
Второе объявление помогает транслятору правильно обрабатывать предложения, в которых производится обращение к полям данных сегмента data. Ранее уже отмечалось, что для обращения к памяти процессору необходимо иметь две составляющие адреса: сегментный адрес и смещение. Сегментный адрес всегда находится в сегментном регистре. Однако в процессоре два сегментных регистра данных, ds и es, и для обращения к памяти можно использовать любой из них. Разумеется, процессор при выполнении команды должен знать, из какого именно регистра он должен извлечь сегментный адрес, поэтому команды обращения к памяти через регистры ds или es кодируются по-разному. Объявляя соответствие сегмента data регистру ds, мы предлагаем транслятору использовать вариант кодирования через регистр ds.
Однако отсюда совсем не следует, что к моменту выполнения команды с обращением к памяти в регистре ds будет содержаться сегментный адрес требуемого сегмента. Более того, можно гарантировать, что нужного адреса в сегментном регистре не будет. Директива assume влияет только на кодирование команд, но отнюдь не на содержимое сегментных регистров. Поэтому практически любая программа должна начинаться с предложений, в которых в сегментный регистр, используемый для адресации к сегменту данных (как правило, это регистр ds) заносится сегментный адрес этого сегмента. Так сделано и в нашем примере с помощью двух команд

mov ax,data ;настроим ds
mov ds,ax ;на сегмент данных

с которых начинается наша программа. Сначала значение имени data (т.е. адрес сегмента data) загружается командой mov в регистр общего назначения процессора ах, а затем из регистра ах переносится в регистр ds. Такая двухступенчатая операция нужна потому, что процессор в силу некоторых особенностей своей архитектуры не может выполнить команду непосредственной загрузки адреса в сегментный регистр. Приходится пользоваться регистром ах в качестве "перевалочного пункта".
Поместив в регистр ds сегментный адрес сегмента данных, мы получили возможность обращаться к полям этого сегмента. поскольку в программе может быть несколько сегментов данных, операционная система не может самостоятельно определить требуемое значение ds, и инициализировать его приходится "вручную".
Назначением нашей программы является вывод на экран текстовой строки " -=* Hello World *=-", описанной в сегменте данных.
Следующие предложения программы как раз и выполняют эту операцию. Делается это не непосредственно, а путем обращения к служебным программам операционной системы MS-DOS, которую мы для краткости будем в дальнейшем называть просто DOS. Дело в том, что в составе команд процессора и, соответственно, операторов языка ассемблера нет команд вывода данных на экран (как и команд ввода с клавиатуры, записи в файл на диске и т.д.). Вывод даже одного символа на экран в действительности представляет собой довольно сложную операцию, для выполнения которой требуется длинная последовательность команд процессора. Конечно, эту последовательность команд можно было бы включить в нашу программу, однако гораздо проще обратиться за помощью к операционной системе. В состав DOS входит большое количество программ, осуществляющих стандартные и часто требуемые функции - вывод на экран и ввод с клавиатуры, запись в файл и чтение из файла, чтение или установка текущего времени, выделение или освобождение памяти и многие другие.
Для того, чтобы обратиться к DOS, надо загрузить в регистр общего назначения ah номер требуемой функции, в другие регистры - исходные данные для выполнения этой функции, после чего выполнить команду hit 21h (int - от interrupt, прерывание), которая передаст управление DOS. Вывод на экран строки текста можно осуществить функцией 09h, которая требует, чтобы в регистрах ds:dx содержался полный адрес выводимой строки. регистр ds мы уже инициализировали, осталось поместить в регистр dx относительный адрес строки, который ассоциируется с именем поля данных msg. Длину выводимой строки указывать нет необходимости, так как функция 09h DOS выводит на экран строку от указанного адреса до символа доллара, который мы предусмотрительно включили в выводимую строку. Заполнив все требуемые, для конкретной функции регистры, можно выполнить команду int 21h, которая осуществит вызов DOS.
Как завершить выполняемую программу? В действительности завершение программы - это довольно сложная последовательность операций, в которую входит, в частности, освобождение памяти, занятой завершившейся программой, а также вызов той системной программы (конкретно - командного процессора command.com), которая выведет на экран запрос DOS, и будет ожидать ввода следующих команд оператора. Все эти действия выполняет функция DOS с номером 4ch. Эта функция предполагает, что в регистре al находится код завершения нашей программы, который она передаст DOS. Если программа завершилась успешно, код завершения должен быть равен 0, поэтому мы в одном предложении mov ax,4c00h загружаем в ан 4ch, а в al - 0, и вызываем DOS уже знакомой нам командой int 21h.

При загрузке программы сегменты размещаются в памяти, как показано на рисунке.

Образ программы в памяти начинается с сегмента префикса программы (Program Segment Prefix, PSP), образуемого и заполняемого системой. PSP всегда имеет размер 256 байт; он содержит таблицы и поля данных, используемые системой в процессе выполнения программы. Вслед за PSP располагаются сегменты программы в том порядке, как они объявлены в программе. Сегментные регистры автоматически инициализируются следующим образом: ES и DS указывают на начало PSP (что дает возможность, сохранив их содержимое, обращаться затем в программе к PSP), CS - на начало сегмента команд, a SS - на начало сегмента стека. В указатель команд IP загружается относительный адрес точки входа в программу (из операнда директивы end), а в указатель стека SP - величина, равная объявленному размеру стека, в результате чего указатель стека указывает на конец стека (точнее, на первое слово за его пределами).
Таким образом, после загрузки программы в память адресуемыми оказываются все сегменты, кроме сегмента данных. Инициализация регистра DS в первых двух строках программы позволяет сделать адресуемым и этот сегмент.

Это еще раз подчеркивает важнейшую особенность архитектуры процессоров intel: адрес любой ячейки памяти состоит из двух слов, одно из которых определяет расположение в памяти соответствующего сегмента, а другое - смещение в пределах этого сегмента. Смысл сегментной части адреса, хранящейся всегда в одном из сегментных регистров, в реальном и защищенном режиме различен; в МП 8086 сегментная часть адреса, после умножения ее на 16, определяет физический адрес начала сегмента в памяти.
Отсюда следует, что сегмент всегда начинается с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Сегментный адрес можно рассматривать, как номер параграфа, с которого начинается данный сегмент. Размер сегмента определяется объемом содержащихся в нем данных, но никогда не может превышать величину 64 кбайт, что определяется максимально возможной величиной смещения.
Сегментный адрес сегмента команд хранится в регистре cs, а смещение к адресуемому байту - в указателе команд ip. Как уже отмечалось, после загрузки программы в ip заносится смещение первой команды программы; процессор, считав ее из памяти, увеличивает содержимое ip точно на длину этой команды (команды процессоров intel могут иметь длину от 1 до 6 байт), в результате чего ip указывает на вторую команду программы. Выполнив первую команду, процессор считывает из памяти вторую, опять увеличивая значение ip. в результате в ip всегда находится смещение очередной команды, т. е. команды, следующей за выполняемой. Описанный алгоритм нарушается только при выполнении команд переходов, вызовов подпрограмм и обслуживания прерываний.
Сегментный адрес сегмента данных обычно хранится в регистре ds, a смещение может находится в одном из регистров общего назначения, например, в dх или si. Однако в МП 8086 два сегментных регистра данных - ds и es. Дополнительный сегментный регистр es часто используется для обращения к полям данных, не входящим в программу, например к видеобуферу или системным ячейкам. Однако при необходимости его можно настроить и на один из сегментов программы. в частности, если программа работает с большим объемом данных, для них можно предусмотреть два сегмента и обращаться к одному из них через регистр ds, а к другому - через es.

суббота, 23 мая 2009 г.

Goodbye debugger :)

В результате экспериментов с сегментами, программа "Hello World", мутировала в программу "Goodbye debugger" :)
Вывел два штамма, далее было лень, так как надо изучать ассемблер. А то получилось, что язык еще не изучил, а сносить крышу отладчикам уже научился. В общем, случайным образом, «открыл» пару антиотладочных приемов.
Итак первого мутанта в студию :)

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

Этот скриншот еще до выполнения первой команды:

А вот что получаем по ходу выполнения:

А это то, что выводит програ вместо сообщения Hello World, под отладчиком:

Забавненько, да?!

Теперь посмотрим на второго монстрика. Эта штучка напрочь выносит отладчик, завешивая его:


Исполнение третье команды приводит вот к такому результату:

После чего отладчик вываливается в командную строку :)

Вот так и свернули голову отладчику, даже не научившись еще программировать :)

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

пятница, 22 мая 2009 г.

Директива MODEL

Сегодня поиграем с упрощенными директивами сегментации. Итак прога:

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

Обращаем внимание на то что выделено красным, а так же что хотя мы определили данные и стек перед кодом, упрощенные директивы TASM, расположили сперва код, потом данные, потом стек. Причем хотя для данных в листинге видим выравнивание типа WORD, по факту TASM сделал выравнивание типа PARA, что видно на скрине экзешника. Так же стоит помедитировать на идентификаторы (Symbol Name) созданые TASM.

Теперь посмотрим как эту прогру оттранслировал MASM:

MASM обошёлся с атрибутами выравнивания более праведным образом. Из скрина EXE видно, что данные выравнены в соответствии с атрибутом WORD, что соответствует листингу программы. Соответственно размер программы оттранслированной MASM меньше на 14 байт, чем то, что сделал TASM.
Теперь побыстрому проги в отладчик и медитируем :)

Еще стоит заметить, что при использовании упрощенных директив сегментации, сегмент стека, хоть и определяется, но не создается в исполняемых файлах на диске. Он создается только в памяти.

Ну и на последок, так же прога но в режиме IDEAL TASM.

TASM оттранслировал ее с тем же размером, что и в режиме совместимости с MASM. В режиме IDEAL, как помним, чуток изменены названия директив определения сегментов.

четверг, 21 мая 2009 г.

Опять игры с сегментами

При попытках понять как работают компиляторы, отладчики и прочая сказочная живность начинаешь обнаруживать интересные вещи. И становится не понятно, то ли это ошибки компиляторов и отладчиков, то ли это их фишки :)
Итак ближе к монитору. Новая серия "Hello World". Нас ждут невероятные приключения в дебрях кода супер программы.

Итак нового мутанта в студию:

Казалось бы, что тут особенного?! В принципе так оно и есть, но вот TASM и MASM по разному компилируют данную программу, но это в принципе не самая интересная особенность. Интересно, то, что компилятор TASM инициализирует не правильным значением регистр SP (указатель стека). Происходит это видимо потому, что мы указали тип выравнивания для стека DWORD, что привело к тому, что стек начинается не на границе параграфа. Соответственно часть стека потерялась, то есть она ни когда не будет использована. Похоже что это глюк TASM. Смотрим следующий скриншот, и погружаемся в глубокую медитацию.

Замечаем, что отладчик использует стек отлаживаемой программы для своих целей. Это может ему дорого стать :) если со стеком что-нибудь сотворить. Но об этом в следующей серии нашего сериала.
Выпуск серий чуть опаздывает от моего изучения ассемблера и исследования поведения компиляторов и отладчиков.
И так, на что следует обратить внимание на скриншоте. Во первых, TASM абсолютно правильно, скомпилил EXE файл, в соответствии с теми атрибутами выравнивания сегментов что мы задавали, НО вот при загрузке в память программы регистр SP получил не правильное значение, и указывает не на дно стека (вернее не на перове слово за концом стека), а указывает на начало параграфа, считая что стек начинается на границе парагарфа, но у нас он не начинается на границе параграфа! Вот такой вот глюк, ну или фишка :). Второе это, то, что отладчик помещает на дно стека два нулевых байта в начале своей работы. Зачем он это делает? Не знаю... Видимо ему это нужно :)
Из всего этого вывод, что работу компиляторов надо проверять :) Не факт, что они создадут, то что мы задумывали :)
Теперь посмотрим как MASM откомпилил нашу прогу:

MASM просто забил на наш атрибут DWORD для выравнивания стека. Он все равно расположил его, как видим, на границе параграфа. В принципе и правильно сделал :) видимо не захотел мучится с высчитыванием смещения дна стека :)
Смотрим все под отладчиком:

Видим, что сейчас с SP все в порядке.

Теперь повторим наш эксперимент с установкой значений SS и SP вручную. Изучаем внимательно текст программы:

И смотрим какой EXE создал TASM:

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

Как видим, наш стек девственно чист, и не осквернен отладчиком :), так как он полагает что стек находится в другом месте :), в конце PSP. Теперь выполним все команды до mov ax,13D5, т.е. команды которые заносят в регистры SS и SP правильные значения.

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

среда, 20 мая 2009 г.

Игры с сегментами

Итак сериал "Hello World" продолжается. Серия уже не помню какая :), но нам же по барабану! :) Мы же фанаты супер гига мега проги всех времен и народов - "Helo World". Итак к монитору! :) Будем воплощать теорию в практику!

Начнем с классики, т.е. со стандартных директив сегментации. И так програ:

Ассемблируем ее:

И получили предупреждение, что стек не определен. И правильно. Вспоминаем описание атрибутов комбинирования сегментов. В частности атрибут stack.

Смотрим расположение сегментов в исполняемом файле:

Все как мы описывали. То есть сегменты располагаются так как мы их описали.

И смотрим расположение сегментов в памяти:

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

Теперь немного поменяем программу вот таким образом:

Транслируем:

Внимательно смотрим листинг:

Смотрим содержимое EXE файлика:

Убеждаемся, что сегменты расположены в порядке, который мы определили в программе. А так же, что сегменты выравнены как мы и указали. Внимательно медитируем на все это, вспоминаем, что было написано про выравнивание и умнеем прямо на глазах :)

Теперь, святое дело, загоняем программу в отладчик:

Скриншот сделан еще до выполнения первой команды программы. Замечаем, что SS установлен на конец PSP, вернее на первое слово за ним, т.е. на начало программы (не путать с точкой входа в программу). Регистр SP вообще не инициализирован. Так же видим, что сегменты располагаются в памяти, в том порядке, как мы их и описали в программе.
Внимательно анализируем значения регистров, углубленно медитируем и просветляемся.
Затем гоняем прогу под отладчиком и становимся еще умнее :)