Требуется ли jr $ ra для завершения программы на языке ассемблера MIPS? (MARS и QtSpim ведут себя по-разному!)

Если вы поставите jr $ra в конце программы на языке ассемблера MIPS в MARS, вы получите сообщение об ошибке:

неверное значение счетчика программы: 0x00000000

Пример 1 ниже не работает:

.data

theMsg: .asciiz "Hello World\n"

.text
.globl main

main:   li $v0, 4       
        la $a0, theMsg  
        syscall         
        
        jr $ra

      

Пример 2 ниже работает:

.data

theMsg: .asciiz "Hello World\n"

.text
.globl main

main:   li $v0, 4       
        la $a0, theMsg  
        syscall

     
    

MARS сообщает, что программа завершена (выпадает снизу), но сообщений об ошибках нет.

Теперь, если вы запустите Пример 2 в QtSpim, вы получите сообщение об ошибке:

Попытка выполнить не-инструкцию по адресу 0x00400030

Если вы запустите Пример 1 в QtSpim, он будет работать нормально.

Может ли кто-нибудь пролить свет на это?
Какой симулятор MIPS подходит?


person Chris    schedule 05.02.2021    source источник
comment
Оба тренажера верны. Оба ваших примера неверны. Чтобы завершить работу программы, необходимо выполнить системный вызов выхода.   -  person fuz    schedule 05.02.2021
comment
@fuz: Похоже, QtSpim передает действительный адрес возврата в $ra на ваш main, позволяя ему возвращаться, как main компилятора C в реальной системе. (SPIM и MARS являются игрушечными системами, а также имеют свои собственные системные вызовы, в отличие от GNU / Linux MIPS или чего-то подобного). Но да, системный вызов выхода, я думаю, более стандартен для этих эмуляторов, поскольку нигде нет _start точки входа, к которой вы могли бы вернуться.   -  person Peter Cordes    schedule 05.02.2021
comment
Спасибо всем. Изменение сработало как для MARS, так и для QtSpim. Я использовал код 10 и сделал системный вызов.   -  person Chris    schedule 06.02.2021


Ответы (2)


Стандартный способ работает везде - это выполнить системный вызов exit (0):

   li $v0, 10         # call number
   syscall            # exit()

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

Это также более реалистично для реальных рукописных asm-программ, работающих в основной ОС, такой как Linux, а не только для игрушечной системы, моделируемой MARS / SPIM.

(В отличие от Linux exit(int), игрушечный системный вызов MARS / SPIM не принимает статус выхода в $a0 или где-либо еще. Он просто завершается.)


В MARS очевидно, что падение со дна является допустимым вариантом для игрушечной системы, которую он имитирует. Однако это никогда не работает ни в одном реальном аппаратном процессоре; всегда есть что-то следующее в памяти, и ЦП попытается извлечь и выполнить это 1.

Ни MARS, ни SPIM не пытаются эмулировать настоящую ОС, такую ​​как Linux, просто предоставляют свою собственную специфическую среду 2. Системы, моделирующие MARS и SPIM, имеют некоторые незначительные отличия друг от друга, в том числе и та, которую вы нашли.

Ни один из них не является правильным или неправильным, просто разные: нет реальной среды, которой они пытались бы сопоставить / подражать.

SPIM может даже иметь возможность включить некоторый код ядра или что-то подобное в память моделируемой системы, IIRC. Возможно, я заблуждаюсь, но в противном случае часть обработки системных вызовов могла бы фактически выполняться большим количеством кода MIPS, приближаясь к реальному процессору MIPS, работающему под управлением ОС. (В отличие от MARS, где реализация системного вызова выполняется исключительно на Java внутри симулятора, который вы вызываете через syscall, а не с точки зрения инструкций MIPS и драйверов устройств для имитируемого оборудования.)

В реальной ОС (например, GNU / Linux с gcc и glibc) main будет правильной функцией, вызываемой обычно из точки входа процесса _start (косвенно через __libc_start_main, чтобы сделать еще кое-что для инициализации перед фактическим вызовом main). _start - реальная точка входа в процесс, первая инструкция, которая выполняется в пользовательском пространстве (по модулю динамического связывания), и не является функцией (нигде нет адреса возврата); ваш единственный вариант - выполнить системный вызов выхода (или дать сбой, или продолжать работать вечно). Когда main возвращается, _start передает свое возвращаемое значение (int, таким образом, в $v0) в качестве аргумента библиотечной функции exit который выполняет очистку таких вещей, как очистка буферов stdio, а затем выполняет _exit системный вызов.

Очевидно, SPIM намеревается, что их main метка будет похожа на функцию C main, или, по крайней мере, получит действительный адрес возврата. IDK, если он получает int argc и char *argv[] в $a0 и $a1.

Чтобы jr $ra работал, SPIM должен устанавливать начальный $ra на какой-то адрес, например, ваш main был вызван откуда-то. Вы, вероятно, найдете код, который копирует $ v0 в $ a0, а затем выполняет системный вызов выхода.

Некоторые люди ошибочно используют main в качестве имени для точек входа, которые, к сожалению, не могут вернуться, я думаю, даже при реальной разработке встраиваемых систем. В стандартных наборах инструментов для систем GNU / Linux (gcc / clang) точка входа в процесс по умолчанию называется _start.

main сбивает с толку, потому что это стандартное имя для функции C (вызываемой asm startup stuff), которая может возвращаться. То, из чего вы не можете вернуться, не является функцией, но в C main определенно является функцией. C - это низкоуровневый язык системного программирования Unix / Linux, и многие другие языки построены на основе этой стандартной инструментальной цепочки кода запуска libc и CRT.


Сноска 1: у большинства ISA есть правила о том, как ПК может переходить с 0xffffffc на 0 или что-то еще, поэтому даже размещение кода в самом конце адресного пространства не может заставить его остановиться самостоятельно при достижении конец. Или, если да, то это будет какая-то неисправность, не выход в ОС. (В этом случае MARS или SPIM действуют как ОС, обрабатывая, помимо прочего, syscall инструкции, которые вы выполняете). Обратите внимание, что настоящая ОС на «голом железе» не имеет возможности выйти, а только перезагрузить компьютер или выключить его. Он не работает ни под чем, из чего может вернуться.

Сноска 2. При очень ограниченном количестве системных вызовов, например нет движения курсора, и некоторые системные вызовы, которые делают то, что библиотечные функции (не системные вызовы) будут делать в реальной системе, например int ‹-› строковое преобразование. Но MARS / SPIM предоставляет это только как часть ввода-вывода, а не atoi или sprintf(buf, "%d", num). Вот почему ярлык игрушки применяется, особенно к набору системных вызовов, которые они предоставляют, что очень не похоже на набор системных вызовов Linux.

Но также и такие вещи, как простая растровая графика, которую имеет MARS, и параметр по умолчанию без задержки ветвления, как для MARS, так и для SPIM по умолчанию. Реальные процессоры MIPS имеют слот задержки ветвления, пока MIPS32r6 не переупорядочит коды операций и не предоставит новые инструкции ветвления без задержки.

По крайней мере, MARS (и, возможно, SPIM) имеют довольно ограниченную поддержку констант времени сборки в своем встроенном ассемблере, например вы не можете использовать .equ или msglen = . - msg для вычисления длины msg: .ascii "hello" во время сборки, как вы могли бы это сделать в ассемблере GNU для MIPS.

person Peter Cordes    schedule 05.02.2021
comment
@Chris: О, я пропустил, что MARS не работал с jr $ra, поэтому ни один из ваших вариантов не работал с обоими. Обновил мой ответ, чтобы поместить выход через часть syscall вверху, где будущим читателям будет легче найти его, и поскольку ничто другое не переносится на оба симулятора. - person Peter Cordes; 06.02.2021
comment
+1, и я добавил ответ на long для комментария о SPIM и его механизме включения кода ядра / обработчика исключений. - person Erik Eidt; 06.02.2021

Чтобы добавить к очень хорошему ответу @ Peter:

SPIM может включать код ядра через Simulator- ›Settings-› Load Exception Handler (вы можете выбрать файл или использовать значение по умолчанию), обработчик - это исходный код сборки. По умолчанию используется обработчик по умолчанию (а не без обработчика).

Когда вы пишете такой обработчик, вы можете включать код в .ktext & .kdata, но также включать пользователя .text & .data. Любой обработчик исключений собирается и загружается раньше пользовательского кода.

Стандартный файл обработчика исключений включает - для размещения в пользователе .text - загрузку argc / argv, а затем выполняет jal main, за которым следует системный вызов # 10 (так что это немного похоже на _start в crt0), что означает, что мы можем вернуть (jr $ra) к этому запуску код. Вот почему код пользователя отображается с [00400020] в SPIM, тогда как в MARS ваш код пользователя начинается с 00400000.

SPIM также не сообщает о недостающих символах до выполнения !!! Таким образом, если main не найден, то при выполнении jal main сообщается об отсутствующем символе main.

Однако, если у вас есть действительный символ main, он не обязательно должен быть первым в файле - он может быть где угодно.

Хотя MARS также запускает выполнение в начале .text, в отличие от него обработчик исключений по умолчанию в MARS не предлагает эквивалента _start, поэтому мы должны запускать программы сборки с основным кодом (но нам действительно не нужен символ main) или же поставить там j somewhere. SPIM будет вести себя больше как MARS, если вы откажетесь от использования обработчика по умолчанию.

person Erik Eidt    schedule 06.02.2021
comment
TL: DR: в своих настройках по умолчанию из пользовательского PoV, SPIM вызывает метку main как функцию. Вместо этого MARS не заботится о метках и начинает выполнение с начала кода пользователя. (Эта инструкция может быть jal main и системным вызовом выхода, если вы хотите эффективно написать свой собственный _start.) - person Peter Cordes; 06.02.2021