Порядок записи стека и порядок выполнения кода

Я читал известную книгу 1980-х годов Питера Нортона и Джона Сона, в которых утверждается, по крайней мере, в итальянской версии, что:

учитывая ассемблерный код без определения пространства, зарезервированного для STACK (следовательно, без директивы .STACK в коде), ассемблируя его, связывая его и затем наблюдая его состояния регистров с помощью DEBUG (непосредственно из .EXE-файла), мы имеем следующее:

A> DEBUG TEST_SEG.EXE
-R
AX = OOOO BX = OOOO CX = 0004 DX = OOOO SP = OOOO BP = OOOO SI = OOOO DI = OOOO
DS = 3985 ES = 3985 SS = 3995 CS = 3995 IP = OOOO NV UP EI PL NZ NA PO NC
3995:0000 B44C           MOV       AH, 4C
-

в книге же написано: Стек сейчас на 3995:0, что является стартом программы (CS:O). Это абсолютно не хорошо. Стек никогда не должен находиться рядом с программным кодом. Кроме того, поскольку указатель стека находится в SS:O, ему некуда расти (по мере уменьшения стека). По этим причинам вы должны определить сегмент стека для программ .EXE.

Сейчас я провел несколько тестов и понял, что стек растет вниз (т.е. например, с 0000h, FFFEh, FFFCh, FFFA и т.д.), затем от старшего адреса к низу (самый низкий адрес). Вместо этого указатель инструкций (IP) увеличивается от самого младшего адреса (в примере от 0000h) к более высоким адресам. По вставке данных в стек и добавлению кода в программу эти двое не встретятся (по крайней мере на время) так как есть запас памяти 64К.

Итак, эта программа .EXE ведет себя более или менее так, как если бы она была .COM, на мой взгляд.

Верно ли то, что написано в книге (в данном случае я что-то упускаю), или то, что я пережил, на самом деле соответствует истине и поэтому в книге (по крайней мере, в итальянской версии) есть ошибка?


person Roberto Rocco    schedule 07.09.2020    source источник
comment
Вы правы: стек находится чуть ниже сегмента кода. Я не помню, как DOS инициализировал EXE-файлы, хотя стек может быть близко к PSP или в незарезервированной области. Кстати, остерегайтесь очень старых книг и их переводов.   -  person Margaret Bloom    schedule 07.09.2020
comment
@MargaretBloom В отличие от файлов .COM (у которых нет заголовка), начальная позиция указателя стека указывается в заголовке каждого файла .EXE.   -  person Martin Rosenau    schedule 07.09.2020
comment
Стек увеличивается до более низких адресов и сжимается до более высоких адресов динамически по мере выполнения программы. Однако код увеличивается только во время компиляции/компоновки, а не во время выполнения: после запуска программный код имеет фиксированный размер. Регистр IP отслеживает, какую инструкцию выполняет процессор, динамически. Иногда мы называем текущий конец кода счетчиком местоположения, чтобы отличить концепцию времени компиляции растущего кода от динамической концепции указателя инструкций.   -  person Erik Eidt    schedule 07.09.2020
comment
Я не помню деталей распределения памяти DOS, но можем ли мы быть уверены, что 3995:FFFE действительно доступен для использования этой программой, а не выделен другой? Если нет, возможно, вы просто перезаписали чужой код или данные.   -  person Nate Eldredge    schedule 07.09.2020
comment
@NateEldredge Поля в заголовке EXE могут гарантировать, что место было выделено для программы. Если компоновщик выделил место для стека, как предполагает ответ rcgldr, то поля, по-видимому, будут установлены для выделения этого пространства. Однако я не уверен, действительно ли это сделали компоновщики MS-DOS.   -  person Ross Ridge    schedule 07.09.2020
comment
@RossRidge - пространство стека по умолчанию - это то, что делают 16-битные компоновщики Microsoft (MSDOS).   -  person rcgldr    schedule 08.09.2020


Ответы (3)


TL;DR: книга Питера Нортона верна. Вы должны определить стек с помощью директивы .STACK при создании программы DOS EXE (MZ), чтобы избежать неопределенного поведения. В качестве альтернативы вы можете создать сегмент с именем STACK с классом STACK, используя директивы SEGMENT/ENDS с соответствующим оператором BYTE ### DUP(?), где ### — размер стека в байтах.


У меня есть английская версия книги (3rd Edition), что похоже на то, что вы цитируете:

DOS всегда устанавливает указатель стека на самый конец сегмента при загрузке COM-файла в память. По этой причине вам не нужно объявлять сегмент стека (с .STACK) для COM-файлов. Что произойдет, если вы удалите директиву .STACK из TEST_SEG.ASM?

C>DEBUG TEST_SEG.EXE
R
AX=0000 BX=0000 CX=0004 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=3985 ES=3985 SS=3995 CS=3995 IP=0000 NV UP EI PL NZ NA PO NC 
3090:0000 B44C MOV AH,4C

Теперь стек находится на 3995:0, что является началом вашей программы (CS:0). Это очень плохие новости. Вам не нужен стек рядом с кодом вашей программы. Поскольку указатель стека находится в SS:0, ему некуда расти (поскольку стек растет в памяти). По этим причинам вы должны объявить сегмент стека для EXE-программ.

Формат программы DOS EXE (MZ) включает заголовок, который включает поля, определяющие начальное значение стека < em>СС:СП. Значением SP будет размер стека, запрошенный директивой .STACK. Если в директиве .STACK не указано значение, обычно по умолчанию используется значение 1024 байта (0x400 байт) для большинства MASM и совместимых ассемблеров.

Когда вы укажете директиву .STACK, компоновщику будет предложено сгенерировать значение SS:SP, которое не конфликтует с другими сегментами в программе EXE.

Формат файла DOS EXE позволяет перемещать сегменты при загрузке программы в память. Значение SS, записанное в заголовок для указателя стека, является значением относительно CS, которое изначально установлено равным нулю. Когда загрузчик DOS считывает программу EXE в память, он выполняет исправления программы, включая значение SS:SP в заголовке. Если, например, у вас есть программа DOS EXE, где SS:SP в заголовке компоновщик устанавливает на 0x0002:0x0400, а загрузчик DOS загружает вашу программу в сегмент 0x3995, тогда стек (SS :SP) будет установлено значение 0x3995+2:0x0400 = 0x3997:0x0400.

Компоновщик указывает, сколько памяти требуется программе, и записывает соответствующую информацию в заголовок. Когда загрузчик DOS читает заголовок, он проверяет, достаточно ли памяти, включая сегмент стека.


Что происходит, когда вы не используете .STACK в программе EXE?

Если .STACK не указано в вашем ассемблерном коде, значение, записанное компоновщиком в поля SS:SP в заголовке, будет установлено на 0x0000:0x0000. Это означает, что когда загрузчик DOS перемещает сегмент и устанавливает SS:SP, эффективное место в памяти будет таким же, как CS:0x0000. Это означает, что если в вашем сегменте CS содержится 65 536 байт кода (достаточно, чтобы заполнить весь сегмент), ваш стек будет увеличиваться поверх него. Например, если вы поместите 16-битное значение в стек, из указателя стека будет вычтено 2, а значение будет записано в это место. Это будет CS:0xFFFE. Кроме того, на самом деле нет никакой гарантии, что память CS:0xFFFE и CS:0xFFFF доступна вашей программе! Когда вы не указываете стек, он не влияет на размер программы, записываемой компоновщиком в поля заголовка. Когда загрузчик DOS считывает его в память, он не будет знать, достаточно ли памяти для вашего стека или нет.

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

Что предлагает Питер Нортон, так это то, что вы всегда должны использовать директиву .STACK или явно определять свой собственный сегмент стека, используя директиву SEGMENT/ENDS с соответствующим количеством байтов, выделенных для него. Это делается для того, чтобы ваша программа загружалась DOS так, как вы ожидаете, и выполнялась так, как вы ожидаете.


Преобразование программ EXE в COM с помощью ранних инструментов разработки

Одной из причин, по которой вы можете захотеть создать EXE-файл без стека, является использование старых версий MASM и компоновщика, который не может напрямую генерировать COM-программу. В ранних версиях MASM не было модели TINY. Чтобы сгенерировать COM-программу, вы создали программу модели SMALL, в которой не указан ни стек, ни перемещение сегментов, а затем вы использовали компоновщик для генерации EXE-программы. Если программа EXE соответствует требованиям программы COM, ее можно преобразовать из EXE. Программа EXE2BIN могла попытаться сделать такое преобразование. COM-программа DOS при первоначальной загрузке имеет стек (SS:SP), установленный на адрес памяти, выровненный по последнему абзацу, доступный в конце сегмента кода. Затем он помещает 0x0000 в стек. Это должно было сохранить совместимость с CP/M. Загрузчик DOS помещает значение 0x0000 в стек, чтобы вы могли выполнить RET для завершения программы. Адрес CS:0x0000 находится в DOS PSP и содержит Int 0x20 инструкция для завершения программы.

При загрузке COM-программы DOS: если загрузчик DOS обнаружит, что доступно все 64 КБ памяти, значение SS будет установлено на сегмент DOS Префикс сегмента программы (PSP) установлен, и он установит SP в 0x0000 и поместит 0x0000 в стек. Вот почему вы часто увидите начальный SS:SP в программе DOS COM, начинающейся с CS:0xFFFE при просмотре в отладчике.

Я не знаю, почему в книге Питера Нортона значение SP равно 0xFFEE (SP=FFEE) в выводе трассировки отладки. Выглядит необычно, но по-прежнему актуален. Это может быть версия DOS, которую он использовал; объем доступной памяти; или его отладчик поместил что-то еще в верхние 16 байтов выше адреса возврата 0x0000.

person Michael Petch    schedule 07.09.2020

Теперь стек находится на 3995:0, что является началом программы (CS:0). Это абсолютно не хорошо. Стек никогда не должен находиться рядом с программным кодом.

Пока стек стоит перед кодом, это абсолютно не проблема:

Стек растет вниз, и сначала уменьшается указатель стека. (Есть типы ЦП, в которых push сначала записывает значение, а затем изменяет SP; на таких ЦП это будет проблемой.)

Таким образом, не будет проблемой, если и начальный SS:SP, и начальный CS:IP равны 0:7C00. (Это типичная комбинация для загрузочных секторов.)

Кроме того, поскольку указатель стека находится в SS:0, ему некуда расти.

Это верно:

В некоторых режимах работы процессоры x86 не допускают push (или call...), если SP равен 0.

Вместо переноса на 0xFFFE программа может просто вылететь.

И, конечно же, у вас есть еще одна проблема: если программа длиннее 64 КБ и CS=SS, операция push перезапишет программный код, если SP переходит от SS:0 к SS:0xFFFE.

person Martin Rosenau    schedule 07.09.2020
comment
В вопросе упоминается 16 бит, что, как я предполагаю, означает реальный режим (в отличие от защищенного режима 286, и даже тогда sp == 0 уменьшается до fffe перед нажатием). Если программа длиннее 64 КБ, то необходимо несколько сегментов, и компоновщик по-прежнему будет предоставлять стек по умолчанию, если в коде или командной строке не указан размер стека. - person rcgldr; 07.09.2020

Поскольку это 16-битный код реального режима, если ss:sp = 3395:0000, то фактически он находится в конце сегмента кода, поскольку sp уменьшается до fffe перед любой отправкой.

Поскольку в исходном коде или командной строке не указан размер стека, компоновщик по умолчанию помещает стек в конец сегмента кода.

person rcgldr    schedule 07.09.2020