Советы и подсказки по программированию на ассемблере

Я пытаюсь написать свою собственную "игрушечную" ОС, и на данный момент я делаю это в основном на сборке (NASM) - отчасти потому, что я надеюсь, что это поможет мне разобраться в разборке x86, а также потому, что я нахожу это тоже довольно весело!

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

В данный момент, в частности, я борюсь с:

Отслеживание регистров

На данный момент все работает в 16-битном режиме, и поэтому у меня есть только 6 регистров общего назначения, с еще меньшим количеством из них, используемых для доступа к памяти. Я продолжаю топтать свои собственные регистры, что, в свою очередь, означает, что я часто обмениваю регистры вокруг, чтобы избежать этого - следовательно, мне трудно отслеживать, какие регистры содержат какие значения, даже с либеральными комментариями. Это нормально? Могу ли я что-нибудь сделать, чтобы легче было отслеживать?

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

; ================
; c_lba_chs
; Converts logical block addressing to Cylinder / Head / Selector
;  ax (input, clobbered) - LBA
;  ch (output) - Track number (cylinder)
;  cl (output) - Sector number
;  dh (output) - Head number
; ================

Отслеживание стека

В нескольких случаях я начал использовать стек, когда у меня заканчивались регистры, но это сильно ухудшает ситуацию - что-то более сложное, чем простое push call pop Последовательность для сохранения регистров приводит к тому, что я полностью теряю трек, и мне сложно даже определить, есть ли у меня нужное количество элементов в стеке (особенно, когда происходит обработка ошибок - см. ниже), не говоря уже о том, в каком порядке они находятся. Я знаю, что должен быть лучший способ использования стека, я просто не вижу, что это такое.

Обработка ошибок

Я использовал флаг переноса и флаг нуля (в зависимости от функции), чтобы указать ошибку вызывающей стороне, например:

myfn:
    ; Do things
    jz .error
    ; Do more things
    ret

    .error:
        stc
        ret

Это нормальный способ указания ошибок?

Также есть ли другие советы или хитрости, которые я могу использовать, чтобы лучше структурировать мою сборку?

Наконец, есть ли хорошие ресурсы / примеры хорошо написанных сборок? Я сталкивался с языком программирования на языке ассемблера, однако он, кажется, сосредоточен на мелочах языка с меньшим акцентом на том, как должен быть структурирован код. (Также в некоторых примерах кода используются сегменты, которых, я думаю, следует избегать).

Я делаю все это, используя нулевые сегменты (модель с плоской памятью), чтобы упростить задачу и упростить ее, если / когда я начну использовать C.

1 ответ

Решение

Не волнуйтесь, вы на правильном пути. Будучи сборщиком, вы можете делать то, что хотите, чтобы у вас была свобода решать, как вы хотите управлять своими регистрами и данными. Я бы порекомендовал разработать какой-то стандарт для себя, и использование C-подобного стандарта может быть неплохой идеей. Я также рекомендовал бы использовать другой язык ассемблера для первого проекта, такого как этот (например, ARM, запущенный на qemu), x86 несколько ужасен, когда идут наборы инструкций. но это отдельная тема...

Ассемблеры обычно позволяют вам объявлять переменные, если хотите, память с именами:

bob: .word 0x1234

Затем от ассемблера (используя ARM asm здесь)

ldr r0,bob
add r0,#1
str r0,bob

Регистры используются временно, реальные данные хранятся в памяти. Такая модель может помочь отслеживать вещи, так как реальные данные хранятся в памяти с созданными пользователем именами переменных, как язык высокого уровня. x86 делает это еще проще, так как вы можете выполнять операции с памятью и не должны проходить регистры для всего. Точно так же вы можете управлять этим с помощью стекового фрейма, чтобы локальные переменные вычитали некоторое число из стека, чтобы покрыть ваш стековый фрейм для этой функции, и в рамках этой функции узнайте / запомните, что переменная joe это указатель стека +4, а ted это указатель стека +8, Возможно, используйте комментарии в своем коде, чтобы запомнить, где эти вещи. Не забывайте восстановить указатель стека / фрейм до его точки входа перед возвратом. Этот метод немного сложнее, так как вы используете не имена переменных, а числовые смещения. Но предоставляет локальные переменные и рекурсию и / или некоторую глобальную экономию памяти.

Выполняя эту работу как человек своими глазами и руками (клавиатура и мышь), вы, вероятно, захотите сохранить данные в регистре не дольше, чем то, что может поместиться на экране вашего текстового редактора за один раз, с первого взгляда посмотрите, как переменная перейдет к Затем регистр возвращается к переменной в памяти одним взглядом. Программа / компилятор, безусловно, может отслеживать столько памяти, сколько у нее в системе, гораздо больше, чем у человека. Вот почему компиляторы в среднем генерируют лучший ассемблер, чем люди (в конкретных случаях люди всегда могут подправить или исправить проблему).

Обработка ошибок, вы должны быть осторожны с использованием флагов, мне это почему-то не подходит. Это может быть очень хорошо, прерывания сохраняют флаги, ваш код должен будет сохранять или устанавливать флаги и т. Д. Хм, проблема с флагами заключается в том, что вы должны проверять / использовать это возвращаемое значение сразу после возврата функции, до того, как у вас есть инструкция, которая изменяет флаги. где, если вы используете регистр, вы можете не изменять этот регистр возврата для получения большего количества инструкций, прежде чем вам нужно будет сэмплировать или использовать это возвращаемое значение.

Я думаю, что суть здесь в том, что, посмотрите на правила соглашения о вызовах C, которые компиляторы используют для этого набора команд, и, возможно, другие наборы команд, вы увидите сильное сходство и по уважительной причине. Они управляемы. Имея так мало регистров, вы можете понять, почему соглашения о вызовах иногда идут прямо в стек для всех аргументов, а иногда и для возвращаемых значений. Мне сказали, что в биосе Amiga было специальное соглашение о вызовах для каждой функции биоса, что создавало жесткую и быстродействующую систему, но когда я пытаюсь воссоздать биос в C с помощью компиляторов и / или присоединиться к функциям с помощью ассемблера сложно в лучшем случае. Я уверен, что без хорошей документации по каждой функции это невозможно. В будущем вы можете решить, что хотите этот портативный компьютер, и, возможно, захотите, чтобы вы выбрали общепринятые правила вызова. Вы по-прежнему захотите прокомментировать свой код, чтобы сказать, что параметр 1 - это, а параметр 2 - это и т. Д. С другой стороны, если вы в настоящее время или в прошлом запрограммировали ассемблер x86, вызывающий вызовы DOS и BIOS, вам было бы вполне удобно смотреть вверх каждую функцию в ссылку, и помещая данные в соответствующие регистры для каждой функции. Поскольку были хорошие справочные материалы, было легко настроить каждую функцию.

Другие вопросы по тегам