Напечатать половину пирамиды чисел в сборке
Я должен написать программу на ассемблере, которая читает число и печатает полупирамиду чисел.
то есть: читать 4
Распечатать
1
1 2
1 2 3
1 2 3 4
Я понял, как читать числа, как использовать цикл для печати по одному символу на строку, но я должен использовать внутренние циклы, и я не знаю, как инициализировать другой контур.
.286
.model small
.stack 1024h
.data
.code
mov cx,5
mov bx,5
cosmin:
mov dl,31h
mov ah, 2h
int 21h
mov dl, 0Ah
int 21h
loop cosmin
end
Здесь я попытался сделать треугольник только для одного символа, но я не знаю, как увеличить значения для каждой строки.
.286
.model small
.stack 1024h
.data
.code
mov cx,5
mov bx,5
cosmin:
mov dl,31h
mov ah, 2h
int 21h
mov dl, 0Ah
int 21h
loop cosmin
end
2 ответа
Создать внутренний цикл с другим loop
инструкция, вы должны сначала сохранить состояние внешнего цикла как loop
инструкция использования cx
,
После завершения внутреннего цикла вы должны восстановить указанное состояние.
Невыполнение этого условия приведет к аннулированию счетчика внешнего цикла, в частности, он будет работать постоянно.
Вы можете сохранить счетчик внешнего цикла практически в любом месте, однако, если у вас есть запасной регистр, например bx
, используй это.
Если у вас нет такого регистра, вы всегда можете использовать стек (push
а также pop
) но это, помимо введения ограничения сохранения стека сбалансированным при каждом условии прерывания цикла, обеспечивает доступ к памяти и делает извлечение внешнего счетчика более неуклюжим.
loop
это не то великое наставление, оно ограниченное и медленное.
Плюс к тому, что ваша задача лучше подсчитать, так что я бы полностью избежал ее в пользу счетчика с ручным управлением.
;Assume SI holds the number n (>=1) of rows of the pyramid
mov cx, 1 ;Outer counter (i)
_rows_loop:
mov bx, 1 ;Inner counter (j)
__cols_loop:
;Print number bx
inc bx ;Increment inner counter
cmp bx, cx ;If (j<i) keep looping
jb _cols_loop
;Print new-line
inc cx ;Increment outer counter
cmp cx, si ;If (i<n) keep looping
jb _rows_loop
Если у вас заканчиваются запасные регистры, вы всегда можете использовать стек, но имейте в виду, что это усложняет код прерывания цикла, поскольку все, что вы нажимаете, всегда должно быть сбалансировано с помощью всплывающих окон. Плюс это доступ к памяти.
В крайних случаях вы можете использовать 8-битные регистры, используя cl
а также ch
за cx
а также bx
в приведенном выше коде будет работать для счетчиков, который подходит.
Я оставляю вам задачу поиска алгоритма генерации пирамиды.
Я не знаю, как увеличить значения для каждой строки.
Ну на каждую строчку делаю inc where-the-value-is-stored
(либо есть в каком-нибудь резервном регистре, либо в памяти, если у вас кончились резервные регистры).
Держите в голове или в комментариях резюме, каково ваше текущее распределение регистров, чтобы вы знали, какие запасные или какие уже используются для чего-то.
Убедитесь, что вы выполняете требования ваших внешних вызовов, так как вы можете легко выбрать другой регистр для своего собственного кода, но вы не можете, например, изменить int 21h
взять служебный номер в bh
, поскольку он уже реализован вашим поставщиком DOS для принятия номера услуги в ah
, Так что либо избегай ah
использовать для себя, или использовать шаблон сохранения / восстановления (ниже).
Постарайтесь, чтобы простые вещи были простыми, например, приращение значения inc
, Сборка на самом деле очень хороша в этом, если держать в голове четкое представление о том, что вы хотите, с точки зрения очень простых числовых операций / шагов, вы обычно можете найти довольно простую и простую комбинацию инструкций ASM, выполняющих именно это и не очень много остальное. Довольно часто ничего другого.
Если вам трудно сопоставить какое-то желание с несколькими инструкциями по сборке простым способом, ваша высокоуровневая задача, вероятно, недостаточно разбита на простые шаги, поэтому попробуйте разбить ее немного подробнее, а затем попробуйте снова найти короткий прямой перевод на инструкции.
loop rel8
это одна из немногих более сложных инструкций x86, делающая в основном это:
dec cx (ecx in 32b mode, rcx in 64b mode)
jnz rel8
Но это не повлияет на флаги (dec + jnz
случается внутри как отдельная вещь, а не как две dec + jnz
инструкции), и на современных процессорах x86 искусственно медленно, чтобы помочь немного устаревшему ПО, которое использовало пустое loop $
Циклы для создания "задержек" (это бесполезно, поскольку для такого ПО он все еще слишком быстр, и он "удаляет" в противном случае очень хороший код операции для будущего ПО:/).
Таким образом, вы можете предпочесть две инструкции " dec cx
jnz rel8
"Сочетание для реального программирования, оно будет иметь лучшую производительность на современном процессоре x86.
В сборке ЦП регистры похожи на "суперглобалы", т.е. есть один cx
на ядро процессора (внутренне это не так на современном x86, но именно так оно и выглядит с точки зрения программиста).
Поэтому, если вам нужны два разных значения, таких как counter1 и counter2, вам придется написать дополнительный дополнительный код, сохраняя соответствующий cx
значение там, где это необходимо, и загрузка другого при необходимости.
Например, два вложенных цикла выполняются loop
:
mov cx,10
outer_loop:
mov bx,cx ; preserve outer counter in bx
mov cx,5
inner_loop:
; some loop code
loop inner_loop
mov cx,bx ; restore outer counter
loop outer_loop
Или, если вам не хватает резервных регистров, вы можете использовать стек, "наивный" способ:
mov cx,10
outer_loop:
push cx ; preserve outer counter
mov cx,5
inner_loop:
; some loop code
loop inner_loop
pop cx ; restore outer counter
loop outer_loop
(Компиляторы C++ решали бы это по-другому, выделяя локальную переменную в пространстве стека, поэтому вместо push / pop она использовала бы ту же самую область памяти [sp+x]
или же [bp-x]
напрямую, сохраняя производительность, не регулируя sp
с каждым использованием, как push/pop
делает)
Но если вы посмотрите на предыдущую часть моего ответа, вы сможете найти другой способ решения вложенных циклов с двумя счетчиками - без дополнительных инструкций сохранения / восстановления.
Но этот шаблон значения сохранения / восстановления в конкретном целевом регистре - это то, что вам необходимо полностью понять и использовать в любых ситуациях (даже если это не нужно для вложенных циклов), например, если вы будете читать документацию о ah=2, int 21h
видишь заботится только о ah
а также dl
значения (и изменяет al
). Так например dh
"запасной".
Тогда, если вы хотите вывести два символа: A
и пространство, но вы все еще хотите покончить с A
в основной "переменной" (будет dl
в следующем примере), вы можете сделать это:
init_part:
mov dx,' '*256 + 'A' ; dh = ' ', dl = 'A'
mov ah,2 ; output single char service
; some other init code, etc..
inner_part_somewhere_later:
int 21h ; output dl to screen (initially 'A')
xchg dl,dh ; preserves old "dl" and loads "dh" into it (swaps them)
int 21h ; output dh to screen (space)
xchg dl,dh ; restores 'A' in dl
; so *here* you can operate with 'dl'
; as some inner_part loop "variable"
; modifying it for another inner_part iteration
Наконец, если у вас есть задача, подобная вашей, а решение не очевидно, одним из обоснованных шагов может быть "возврат" того, что вы хотите.
Вы знаете, что хотите вывод на экран (<NL>
= новая строка):
1<NL>
1 2<NL>
Итак, представьте, что это значит на нижнем уровне, в конце концов. Есть, конечно, несколько способов, как этого добиться (включая запись целых строк, подготовленных в буфере памяти, а не одиночных символов), но если я буду придерживаться вывода с одним символом, этот требуемый вывод преобразуется в эту потребность:
Звонить int 21h, ah=2
с dl
установлен в:
[ 49
(цифра 1), 13
(возврат каретки), 10
(перевод строки), 49
, 32
(пространство), 50
(цифра 2), 13
, 10
].
Это не выглядит явно "зацикленным", но если вы добавите больше строк, появится шаблон "цифра + пробел" для внутреннего цикла. Вы также можете немного "обмануть" и вывести один бесполезный пробел после последней цифры, так как для обычного пользователя он будет "невидимым". На этом этапе вы должны быть в состоянии "вернуться" к этому высокоуровневому дизайну:
char_per_line_count = 1
ending_char_count = 2
[lines_loop:
char = '1'
line_counter = char_per_line_count
[chars_loop:
int 21h,2 with char
int 21h,2 with space
loop to chars_loop while (--line_counter)]
int 21h,2 with 13
int 21h,2 with 10
++char_per_line_count
loop to lines_loop while (char_per_line_count < ending_char_count)]
Теперь вы можете попробовать запустить его несколько раз в своей голове, чтобы убедиться, что вывод действительно соответствует вашим ожиданиям.
Когда у вас будет такой общий обзор того, как вы можете достичь желаемого результата, вы можете начать искать способ, как правильно реализовать определенные его шаги.
Если вы понимали каждую предыдущую часть этого ответа, я думаю, что будет довольно легко переписать этот алгоритм в инструкции ASM. Просто держите комментарии в коде перед определенной группой инструкций.
Затем, когда вы отлаживаете из-за какой-то ошибки, вы можете легко сравнить, что на самом деле делает код с комментарием, что он должен был сделать, найти несоответствие и исправить его.
Но все время главное, с чем сравнивать ваш код, это то, что конечный вывод на экране определен, когда вы застреваете, сравниваете текущий вывод с желаемым, находите некоторое расхождение, оцениваете, какой из них выглядит наиболее простым для исправления, и старайтесь почини это. Если расхождений больше не обнаружено, вы вроде "готово", хотя я настоятельно рекомендую еще раз взглянуть на ваш код, нельзя ли его упростить и правильно ли он работает для угловых случаев (например, "что происходит, если пользователь вводит букву вместо цифры ").
Не важно иметь код, который правильно обрабатывает каждый угловой случай, но вы должны знать, что произойдет в каждом случае, и решить, достаточно ли это хорошо или нет (как правило, "garbage in -> garbage out" "хорошо", мусор в -> сбой или повреждение данных "не круто", мусор в -> значимое исправление или сообщение об ошибке это круто).