Мне кажется, порядок ваших инструкций неверен. Вы делаете сравнение, затем декремент, затем условный переход. Ваши значения флага из сравнения могут быть изменены декрементом.
loop_start:
invoke StdOut, addr ProgramText
cmp loop_stopper, 0
dec loop_stopper
jg loop_start
ret
Когда я занимался программированием на ассемблере, я делал это следующим образом: уменьшал счетчик, а затем зацикливал, если не ноль.
loop_start:
invoke StdOut, addr ProgramText
dec loop_stopper
jnz loop_start
ret
Конечно, в зависимости от процессора, вы можете поместить переменную цикла в регистр, который позволяет уменьшать и выполнять цикл с помощью одной инструкции. (например, инструкция Z80 'djnz'. Я не могу вспомнить, в каком далеком регистре он был, хотя регистр 'B', кажется, звонит в колокол).
Кроме того, как предлагали некоторые другие, вы, похоже, не очищаете пространство памяти. Большинство программ на самом деле «ВЫЗЫВАЕТ» ваш код. Поэтому вам нужно сохранить код и указатели стека, чтобы вы делали изящный «ВОЗВРАТ» для вызывающей части операционной системы. Если вы этого не сделали, ваш «ВОЗВРАТ» может привести вас туда, куда указывает верхушка стека, обычно с катастрофическими последствиями.