Сохранение значения в каждом элементе массива: преобразование цикла C do{}while в MIPS asm

Мне нужно преобразовать эту функцию C++ в сборку MIPS:

int set(int a[], int n, int v)
{
    int i;
    i = 0;
    do {
      a[i++] = v;
    } while ( i < n);
    return i;
}

где адрес массива находится в $a0, n — в $a1, а v — в $a2. Это моя попытка написать функцию в MIPS, но я получаю неопределенный символ ссылок на инструкции по адресу 0x00400054. в main (предоставленном моим профессором) есть вызов jal set, который должен вызывать функцию set, и я почти уверен, что моя ошибка как-то связана с этим. Я также не знаю, успешно ли я преобразовал функцию. Это моя попытка:

.text
set:
    li      $t0, 0
    addi    $t0, $t0, 0
    sll     $t1, $t0, 2
    add     $t1, $t1, $a0
L1: lw      $t1, 4($t1)
    sw      $a2, 0($t1)
    blt     $t0, $a1, L1

Я использую QTSPIM, если это вообще имеет значение. Я ценю любую помощь, и если у вас есть какие-либо советы по программированию MIPS, это тоже было бы здорово.

ОБНОВИТЬ:

Файлы теперь связаны, но я получаю бесконечный цикл исключения, произошедшего на ПК = 0x004000f0, и неверный адрес в данных / чтении стека: 0x00000000. Это мой обновленный файл:

.text
.globl set
set:    addi    $t0, $t0, 0     #i = 0;
         sll    $t1, $t0, 2     #offsets 4 * i
         add    $t1, $t1, $a0       #pointer to a[i]
L1:       lw    $t1, 4($t1)     #a[i++]
          sw    $a2, 0($t1)     #a[i++] = v
         blt    $t0, $a1, L1        #continue the loop as long as i < n

Почему мой код должен быть в .globl? И какова цель .text?


person Pachitoo23    schedule 14.02.2016    source источник
comment
Почему бы не скомпилировать код (gcc -O1 -g -o test.o -c test.cpp), а затем objdump -S test.o?   -  person kfsone    schedule 15.02.2016
comment
Похоже, это было бы обманом. Кроме того, зачем создавать .o только для того, чтобы разобрать его, когда можно создать .s (gcc -O1 -g -o test.s -S test.cpp)?   -  person pat    schedule 15.02.2016
comment
Зачем вам это нужно? Для этого у нас есть компиляторы.   -  person Alan Stokes    schedule 15.02.2016
comment
@AlanStokes: Наверное, домашняя работа!   -  person Claudiu    schedule 15.02.2016
comment
Вам нужно сделать set глобально видимым, чтобы компоновщик мог его видеть. Используйте .globl linux.web.cern .ch/linux/scientific4/docs/rhel-as-en-4/   -  person pat    schedule 15.02.2016
comment
Что ты делаешь с li и addi? Похоже, что addi избыточен. Вы имели в виду addi $t0, $a0, 0? Если это так, то li является избыточным. Как у вас есть, $t0 = 0 после этих строк и $t1 будет 0 после sll. Но ничего из этого не связано с вашей ошибкой неопределенного символа. Нам нужно увидеть (хотя бы часть) кода вашего профессора, чтобы сказать вам, что его вызывает.   -  person Tyler    schedule 15.02.2016
comment
Я не знаю, что делает objdump -S. Я пишу свои .s файлы в блокноте   -  person Pachitoo23    schedule 15.02.2016
comment
Не дешеви ОП. Ваш профессор узнает из собрания, что вы, скорее всего, это не писали.   -  person Tyler    schedule 15.02.2016
comment
Я не ищу ответ. Мне просто нужно руководство, потому что мой профессор с трудом говорит по-английски, а я не понимаю ассемблера. Я пытался научить себя с помощью книги и обучающих видео, но я все еще не понимаю   -  person Pachitoo23    schedule 15.02.2016
comment
@FranciscoEscobar, см. комментарий Пэт. Я думаю, что у него есть ответ.   -  person Tyler    schedule 15.02.2016
comment
[Действительно] хорошее эмпирическое правило: добавляйте комментарий к каждой строке inst [кроме .text, .globl и т. д.], который определяет намерение. То есть, что происходит в проблемно-ориентированном плане. Таким образом, вы можете отслеживать свою логику по сравнению с реализацией. (т. е.) Ваша логика верна или инсталляция неверна?   -  person Craig Estey    schedule 15.02.2016
comment
Куда пойдет .globl в моем коде? Заменит ли он .text?   -  person Pachitoo23    schedule 15.02.2016
comment
.globl set является дополнением к .text и должно следовать за ним. Или, правильнее, он должен предшествовать set: [в другой строке]   -  person Craig Estey    schedule 15.02.2016
comment
Чтобы добавить к комментарию / практическому правилу @Craig, вы также должны включить индекс регистра перед функцией, которая объясняет, как вы используете каждый регистр. Вот пример: pastebin.com/dxN6ZTf7   -  person Tyler    schedule 15.02.2016
comment
.text указывает, что последующее будет связано с сегментом text [для кода]. .globl set говорит сделать метку set глобально видимой для компоновщика. В вашем коде есть вторая метка L1, которую вы хотите сохранить приватной. В C связь по умолчанию для set() является глобальной [если вы не сделаете static int set(...)]. В ассемблерном языке все является закрытым [для файла], если метка также не имеет директивы .globl [в asm нет .static]. В более сложной функции у вас может быть много меток L1, L2, L3,..., которые являются просто внутренними целями перехода. Утомительно помечать их как статические, поэтому статические/частные по умолчанию.   -  person Craig Estey    schedule 15.02.2016


Ответы (1)


Хорошо, было несколько проблем.

Исправленный код внизу. На самом деле есть два способа сделать это, в зависимости от того, насколько буквальным должен быть перевод из кода C. Часть вашей концептуальной проблемы могла заключаться в том, что вы пытались объединить части двух методов.

Вы объединили свои первые две инструкции из исходного кода в одну, основываясь на комментариях [как дубликат] (например, li, затем addi). Но, если используется только один, li правильный, потому что addi добавляет регистр к самому себе, но вы не можете полагаться на то, что начальное значение равно нулю.

sll сдвигает регистр, содержащий нулевое значение, поэтому inst ничего не делает.

Чтобы загрузить t1 с a0, вы должны использовать add $t1,$a0,0 [или add $t1,$a0,$zero]

lw не имело смысла [Код C не загружает a, так почему должен asm?].

Но я немного изменил все это, потому что цикл все равно не работал бы правильно.

Возврата после вашего blt не было, так что даже если цикл сработает, он "упадет с края света". Каждая вызываемая подпрограмма ассемблера [та, которая вызывается как jal set] должна иметь явный оператор возврата, который jr $ra

ПРИМЕЧАНИЕ. В MIPS asm a* [регистры аргументов] может изменяться вызываемым, поэтому зацикливается на a0, а не на t1 (т. е. вызывающий ожидает > что их выкинут)


Во всяком случае, вот исправленный код [пожалуйста, извините за беспричинную очистку стиля]:

    .text

    .globl  set
set:
    li      $t0,0                   # i = 0
L1:
    sw      $a2,0($a0)              # a[i] = v
    add     $a0,$a0,4               # advance pointer
    add     $t0,$t0,1               # ++i
    blt     $t0,$a1,L1              # continue the loop as long as i < n
    jr      $ra                     # return

Если бы ваша исходная функция C была примерно такой:

int
set(int *a, int n, int v)
{
    int *end;

    end = a + n;
    for (;  a < end;  ++a)
        *a = v;

    return n;
}

Тогда это был бы более дословный перевод:

    .text

    .globl  set
set:
    sll     $a1,$a1,2               # convert int count to byte length
    add     $a1,$a0,$a1             # end = a + length

L1:
    sw      $a2,0($a0)              # *a = v
    add     $a0,$a0,4               # ++a
    blt     $a0,$a1,L1              # continue the loop as long as a < end

    jr      $ra                     # return

ИМО, оба метода являются приемлемыми реализациями исходной функции C. Первый более буквален, поскольку сохраняет [концепцию] индексной переменной i. Но у него есть дополнительная инструкция, которой нет у второго.

Оптимизатор, вероятно, будет создавать один и тот же код (то есть второй asm) независимо от того, какую функцию C он транслирует (MIPS не имеет мощных режимов индексной адресации, которые есть у x86 asm).

Итак, что является «правильным», может зависеть от того, насколько приверженцем является ваш профессор.


Примечание. Обратите внимание на изменения стиля между двумя моими примерами. То есть код меняется в сторону, добавляя несколько пустых строк для повышения ясности.


Для полноты картины вот функция main, которую я создал при тестировании:

    .data
arr:    .space  400                 # allocate more space than count

    .text

    .globl  main
main:
    la      $a0,arr                 # get array pointer
    li      $a1,10                  # get count
    li      $a2,3257                # value to store

    jal     set

    li      $v0,1                   # exit syscall number
    syscall

ОБНОВЛЕНИЕ:

Если в цикле a[i++] = v, должен ли я поместить строку add $t0, $t0, 1 перед sw $a2, 0($a0)?

Нет, вы бы сделали это, если бы код C был a[++i] = v. Возможно, лучший способ увидеть это — сначала упростить код C.

a[i++] = v на самом деле:

a[i] = v;
i += 1;

И a[++i] = v на самом деле:

i += 1;
a[i] = v;

Теперь между строками кода C и ассемблерными инструкциями существует однозначное соответствие.

Когда я буду использовать sll? Я читал примеры и заметил, что люди обычно делают sll $t1, $t0, 2, когда собираются использовать счетчик для прохода по массиву.

да. Если вы внимательно посмотрите на мою вторую реализацию, то увидите, что она использует sll таким же образом. Кроме того, я бы закодировал цикл, даже если бы был исходный код C.

Буду ли я использовать lw, если код C говорит что-то вроде int x = a[0]?

Да, точно.


Другой способ создания прототипа ассемблера — преобразовать код C в «очень тупой C».

То есть только if самой простой формы: if (x >= y) goto label. Даже if (x < y) j = 10 запрещено.

Нет переменных области действия функции или переменных аргумента функции — только глобальные переменные, которые являются именами регистров.

Никаких сложных выражений. Только простые, такие как x = y, x += y или x = y + z. Таким образом, a = b + c + d будет слишком сложным.

Регистровые переменные функционируют как целочисленные значения и как байтовые указатели. Таким образом, при добавлении к регистру, используемому в качестве указателя, это похоже на добавление к указателю байта, поэтому для увеличения массива int вы должны добавить 4.

Фактическая разница между указателем байта и указателем int имеет значение только при выполнении операции загрузки/сохранения: lw/sw для int и lb/sb для байта.

Итак, вот моя вторая функция C, перекодированная как "немая":

// RETURNS: number of elements changed
int
set(void)
// a0 -- "a" (pointer to int array)
// a1 -- "n" (number of elements in "a")
// a2 -- "v" (value to set into "a")
{

    v0 = a1;                            // set return value

    a1 <<= 2;                           // convert int count to byte length
    a1 += a0;                           // point to one past end of array

L1:
    *(int *)a0 = a2;                    // store v at current array location
    a0 += 4;                            // point to next array element
    if (a0 < a1) goto L1;               // loop back until done

    return;
}

ОБНОВЛЕНИЕ №2:

В вашей первой реализации add $a0, $a0, 4 эквивалентно использованию sll во второй реализации?

Не совсем. Главное, что нужно помнить, это то, что в C, когда мы добавляем единицу к указателю [или индексируем его с помощью i], компилятор генерирует инструкцию увеличения/добавления, которая добавляет sizeof к типу указателя. определяется как.

То есть для int *iptr указание iptr += 1 сгенерирует add $a0,$a0,4, потому что sizeof(int) равно 4. Если бы у нас были double *dptr, dptr += 1, компилятор сгенерировал бы add $a0,$a0,8, потому что sizeof(double) равно 8.

Это мощное «удобство», которое обеспечивает компилятор C, поскольку он позволяет взаимозаменяемо использовать массивы, указатели и индексы.

В ассемблере мы должны делать вручную то, что компилятор C сделал бы за нас автоматически.

Рассмотрим следующее: у нас есть значение, которое представляет собой количество элементов в массиве, назовите его count. Теперь мы хотим знать, сколько байтов займет массив. Мы назовем это len. Вот код C, чтобы определить это для различных типов:

char *arr;
len = count * sizeof(char);
len = count * 1;
len = count << 0;
//  sll $a1,$a1,0

short *arr;
len = count * sizeof(short);
len = count * 2;
len = count << 1;
//  sll $a1,$a1,1

int *arr;
len = count * sizeof(int);
len = count * 4;
len = count << 2;
//  sll $a1,$a1,2

double *arr;
len = count * sizeof(double);
len = count * 8;
len = count << 3;
//  sll $a1,$a1,3

Насколько я понимаю, использование sll устанавливает i в качестве счетчика для целых чисел, поэтому оно увеличивает i, а также выполняет итерацию массива.

sll - это просто инструкция MIPS "логический сдвиг влево", и вы используете ее, когда вам нужен эквивалент оператора C <<.

Вы думаете о том, как sll можно использовать для достижения такого эффекта.

Чтобы перебрать массив из int, мы увеличиваем index на 1, но мы также должны увеличивать указатель массива на 4. Это то, что делал мой первый пример asm. Условие завершения было index >= count.

В моем втором примере asm я исключил отдельную индексную переменную, преобразовав количество элементов в длину в байтах (через ssl), добавив адрес массива. Теперь $a1 имел адрес последнего элемента массива +1, а условие завершения было current_address >= last_element_plus_1. Обратите внимание, что current_address ($a0) нужно было увеличить на 4 (add $a0,$a0,4)

Важно помнить, что инструкции asm [особенно MIPS] просты (то есть глупы). Они делают только одно дело за раз. Один оператор присваивания C может генерировать около 20 инструкций, если оператор достаточно сложен. Именно то, как ассемблерные инструкции комбинируются, дает более сложные результаты.

person Craig Estey    schedule 15.02.2016
comment
Большое спасибо. У меня есть пара вопросов. Если в цикле a[i++] = v, поместил бы я строку add $t0, $t0, 1 перед sw $a2, 0($a0)? Когда я буду использовать sll? Я читал примеры и заметил, что люди обычно делают sll $t1, $t0, 2, когда собираются использовать счетчик для прохода по массиву. Буду ли я использовать lw, если код C говорит что-то вроде int x = a[0]? Спасибо еще раз за помощь - person Pachitoo23; 15.02.2016
comment
Хорошо, это имеет смысл, еще раз спасибо. Я собираюсь максимально упростить код, чтобы его было легче конвертировать. В вашей первой реализации эквивалентно ли add $a0, $a0, 4 использованию sll во второй реализации? Насколько я понимаю, использование sll устанавливает i в качестве счетчика для целых чисел, поэтому он увеличивает i, а также выполняет итерацию массива, в то время как add $a0, $a0, 4 делает это, но указывает на a[0], затем на a[1] и так далее. Я близко? - person Pachitoo23; 16.02.2016
comment
Обратите внимание, что blt между двумя регистрами является псевдоинструкцией для slt/bnez. Обычно вы хотите сделать bne p, endp, .loop, когда знаете, что ваш конечный указатель будет точно равен в какой-то момент, а не может быть смещен. - person Peter Cordes; 27.03.2021
comment
godbolt.org/z/8WdYTWdva — GCC делает тот же asm с приращением указателя, используя BNE после того, как я изменил с i < n по i != n. (Думаю, из-за возможности отрицательного n; обработка больших n с более эффективным циклом приведет к ветвлению для случая отрицательного-n. Или действительно для n<2, чтобы вы могли развернуться...) - person Peter Cordes; 27.03.2021