Ассемблерный код для возврата наименьшего целого числа в массиве вместо случайного возврата последнего или второго до последнего числа - PullRequest
2 голосов
/ 23 февраля 2020

Я пытаюсь создать функцию в nasm, которая, учитывая массив целых чисел и длину массива, возвращает наименьшее целое число. Это основано на проблеме CodeWars «Найти наименьшее целое число в массиве» . Я делаю это на 64-битном BlackArch Linux. Моя функция выглядит следующим образом:

SECTION .text
global find_smallest_int

find_smallest_int:
  ; [rdi] is the first value in the array.
  ; We'll store the smallest value so far found
  ; in rax. The first value in the array is the
  ; smallest so far found, therefore we store it
  ; in rax.
  mov rax, [rdi]

  ; rsi is the second argument to int find_smallest_int(int *, int)
  ; which represents the length of the array.
  ; Store it in rbx to be explicit.
  mov rbx, rsi

  loop:
    ; Check to see if we've reached the end of the array.
    ; If we have, we jump to the end of the function and 
    ; return the smallest value (which should be whatever
    ; is in rax at the moment.
    cmp rbx, 0
    je end

    ; Subtract one from our counter. This started as 
    ; the number of elements in the array - when it
    ; gets to 0, we'll have looped through the entire thing.
    sub rbx, 1

    ; If rax is smaller than [rdi], we'll jump down to the
    ; rest of the loop. Only if rax is bigger than [rdi] will
    ; we reassign rax to be the new smallest-yet vaue.
    cmp rax, [rdi]
    jl postassign

    assign:
      ; If we execute this code, it means rax was not less
      ; than [rdi]. Therefore, we can safely reassign
      ; rax to [rdi].
      mov rax, [rdi]


    postassign:
    ; Set rdi to point to the next value in the array
    add rdi, 4

    ; if we get here, then we aren't finishing looping yet
    ; because rbx (the counter) hasn't eached 0 yet.
    jmp loop

  end:
    ret

Затем я вызываю эту функцию с помощью следующего C кода:

extern int find_smallest_int(int *array, int size);

int main(void)
{
    int nums[4] = {800, 300, 100, 11};
    int ret = find_smallest_int(nums, 4);

    return ret;
}

Наконец, я компилирую и запускаю все это, используя следующие команды :

#!/bin/bash

# Make an object file from my assembly code with nasm
nasm -f elf64 -o sum.o call_sum.s

# make an object file from my C code
gcc -O0 -m64 -c -o call_sum.o call_sum.c -g

# compile my two object files into an executable
gcc -O0 -m64 -o run sum.o call_sum.o -g

# Run the executable and get the output in the
# form of the exit code.
./run
echo $?

Вместо того, чтобы получать наименьшее целое число, я получаю либо 100, либо 11 (от второго до последнего и последнего члена целочисленного массива, который я передаю своей функции сборки, соответственно). Какой результат я получаю, кажется совершенно случайным. Я могу запустить программу несколько раз и получить 11, затем запустить еще несколько, а затем начать получать 100.

Если кто-нибудь сможет помочь мне понять это странное поведение, я буду очень признателен. Спасибо!

Обновление: Я применил изменения из комментария Джестера (используя 32-битные регистры для хранения целых чисел), и это работает, но я не совсем понимаю, почему.

1 Ответ

4 голосов
/ 09 марта 2020

Начало этого ответа основано на том, что прокомментировал Шут. Это просто расширяет это и объясняет изменения более подробно. Я также внес некоторые дополнительные изменения, два из которых также исправляют ошибки в ваших источниках.

Во-первых, эта часть:

int - это 4 байта, но вы используете 8 все по вашему коду. Используйте eax вместо rax.

Эти инструкции в вашем примере обращаются к 8 байтам из каждого массива:

    mov rax, [rdi]

    cmp rax, [rdi]

    mov rax, [rdi]

Это потому, что rax является 64-битный регистр, поэтому полная загрузка rax или сравнение с операндом памяти осуществляет доступ к 8 байтам памяти. В синтаксисе NASM вам разрешено явно указывать размер операнда памяти, например, записав следующее:

    mov rax, qword [rdi]

Если вы сделали это, вы могли раньше выяснить, что обращались к памяти в 8- байтовые единицы (четырехслойные слова). Попытка явного доступа к двойному слову при использовании rax в качестве регистра назначения не удалась. Следующая строка вызывает ошибку «несоответствие размеров операндов» во время сборки:

    mov rax, dword [rdi]

Следующие две строки в порядке, и обе загружаются в rax из операнда памяти двойного слова. Первый использует нулевое расширение (которое подразумевается в наборе инструкций AMD64 при записи в 32-разрядную часть регистра), второе использует (явное) расширение знака:

    mov eax, dword [rdi]
    movsx rax, dword [rdi]

(A movzx инструкции от операнда памяти dword к rax не существует, так как она будет избыточной с mov для eax.)

Позже в вашем примере вы используете rdi в качестве адреса 4-х байтового типа, передвигая указатель на запись массива, добавляя к нему 4:

    add rdi, 4

Это верно для типа int, но конфликтует с использованием четырех слов в качестве операндов памяти ' размеры.

Еще два вопроса даны комментарием Шута:

Также не используйте rbx, потому что это регистр, сохраненный вызываемым пользователем, и копировать из * 1044 бессмысленно тем не мение. Как и раньше, вам лучше использовать esi, потому что это еще одно целое число.

Проблема rsi заключается в том, что старшие 32 бита 64-битного rsi могут содержать ненулевые значения в зависимости от ABI. Если вы не уверены, разрешено ли ненулевое значение, вы должны предположить, что оно допустимо, и использовать 32-разрядное значение только в esi.

rbx (или ebx ) проблема заключается в том, что rbx необходимо сохранять при вызовах функций для psABI AMD64, который используется Linux, см. Где документирован ABI System V x86-64? для документации этого ABI , В вашей простой тестовой программе изменение rbx может не вызвать каких-либо сбоев, но это легко произойдет в нетривиальном контексте.

Следующая проблема, которую я обнаружил, связана с вашей инициализацией eax. Вы написали это так:

  ; [rdi] is the first value in the array.
  ; We'll store the smallest value so far found
  ; in rax. The first value in the array is the
  ; smallest so far found, therefore we store it
  ; in rax.
  mov rax, [rdi]

Однако, как свидетельствует ваш l oop logi c управления потоком, вы позволяете вызывающей стороне передавать ноль для аргумента размера. В этом случае вам вообще не нужно обращаться к массиву, потому что «первое значение в массиве» может даже не существовать или вообще ничего не инициализировать. Логически, вы должны инициализировать наименьшее значение из INT_MAX вместо первой записи массива.

Есть еще одна проблема: вы используете rsi или esi в качестве числа без знака, считая до нуль. Однако в объявлении функции вы указали тип аргумента size как int, который подписан. Я исправил это, изменив объявление на unsigned int.

Я сделал несколько дополнительных изменений в вашей программе. Я использовал локальные метки NASM для суб-меток вашей функции, что полезно, потому что вы могли бы повторно использовать, например, .loop или .end в других функциях в том же исходном файле, если они были добавлены.

Я также исправил один из комментариев, отметив, что мы прыгаем, когда eax меньше, чем запись массива, и не прыгаем, если eax больше или равна записи массива. Вместо этого вы могли бы изменить этот условный переход на jle, что также привело бы к равным сравнениям. Возможно, один или другой может быть предпочтительнее для ясности или производительности, но у меня нет большого ответа, какой именно.

Я также использовал dec esi вместо sub esi, 1, что не очень много лучше, но лучше сидит со мной. В 32-битном режиме dec esi - однобайтовая инструкция. В 64-битном режиме это не так; dec esi - это 2 байта, а sub esi, 1 - 3 байта.

Кроме того, я изменил начальную проверку для esi с нуля с использования cmp на test, что немного лучше, обратитесь к Проверить, равен ли регистр нулю с помощью CMP reg, 0 против OR reg, reg?

Наконец, я изменил фактическое условие l oop на конец l oop body, что означает, что l oop использует на одну инструкцию перехода меньше. Безусловный переход к началу тела l oop заменяется условным переходом, который проверяет условие while. test в начале функции все еще необходим для обработки возможности массива нулевой длины. Кроме того, вместо использования cmp или test снова для проверки нуля в esi, я просто использую флаг нуля, уже установленный инструкцией dec, чтобы проверить, было ли esi уменьшено до нуля.

Вы можете использовать ecx или rcx для счетчика l oop, но это, вероятно, не будет большой победой на современных процессорах. Если бы вы прибегли к использованию инструкций jrcxz, jecxz или loop, можно было бы использовать немного более компактный код. Однако они не рекомендуются из-за более низкой производительности.

Вместо сравнения с dword [rdi], а затем, если он меньше или равен eax, при загрузке из одного и того же слова памяти, вы можете загрузить сначала значение записи массива в регистр, а затем использовать его как источник для cmp и mov. Это может быть быстрее, но это приводит к большему количеству байтов кода операции.

Хитрость, которую я иначе использую для продвижения Индекса назначения (rdi в 64-битном режиме) на 4, заключается в использовании одного scasd инструкция, которая изменяет только флаги и индексный регистр. Это однобайтовая инструкция вместо 4-байтового add rdi, 4, но, скорее всего, она выполняется медленнее.

Я загрузил репозиторий с вашим исходным кодом и мои улучшения до https://hg.ulukai.org/ecm/testsmal/file/2b8637ca416a/ (Принято в соответствии с CC BY-SA условиями использования содержимого stackoverflow.) Я также изменил часть C и тестовый скрипт, но они тривиальны и в основном не имеют отношения к вашему вопросу. Вот источник сборки:


INT_MAX equ 7FFF_FFFFh

SECTION .text
global find_smallest_int

find_smallest_int:

                ; If the array is empty (size = 0) then we want to return
                ; without reading from the array at all. The value to return
                ; then logically should be the highest possible number for a
                ; 32-bit signed integer. This is called INT_MAX in the C
                ; header limits.h and for 32-bit int is equal to 7FFF_FFFFh.
                ;
                ; If the array is not empty, the first iteration will then
                ; always leave our result register equal to the value in
                ; the first array entry. This is either equal to INT_MAX
                ; again, or less than that.
        mov eax, INT_MAX

                ; esi is the second argument to our function, which is
                ; declared as int find_smallest_int(int *, unsigned int).
                ; It represents the length of the array. We use this
                ; as a counter. rsi (and its part esi) need not be preserved
                ; across function calls for the AMD64 psABI that is used by
                ; Linux, see https://stackoverflow.com/a/40348010/738287

                ; Check for an initial zero value in esi. If this is found,
                ; skip the loop without any iteration (while x do y) and
                ; return eax as initialised to INT_MAX at the start.
        test esi, esi
        jz .end

.loop:
                ; If eax is smaller than dword [rdi], we'll jump down to the
                ; rest of the loop. Only if eax is bigger than or equal to
                ; the dword [rdi] will we reassign eax to that, to hold the
                ; new smallest-yet value.
        cmp eax, dword [rdi]
        jl .postassign

.assign:
                ; If we execute this code, it means eax was not less
                ; than dword [rdi]. Therefore, we can safely reassign
                ; eax to dword [rdi].
        mov eax, dword [rdi]


.postassign:
                ; Set rdi to point to the next value in the array.
        add rdi, 4


                ; Subtract one from our counter. This started as 
                ; the number of elements in the array - when it
                ; gets to 0, we'll have looped through the entire thing.
        dec esi

                ; Check to see if we've reached the end of the array.
                ; To do this, we use the Zero Flag as set by the prior
                ; dec instruction. If esi has reached zero yet (ZR) then
                ; we do not continue looping. In that case, we return the
                ; smallest value found yet (which is in eax at the moment).
                ;
                ; Else, we jump to the start of the loop to begin the next
                ; iteration.
        jnz .loop

.end:
        retn

Вот альтернатива условному переходу внутри тела l oop. Инструкция cmov поддерживается всеми процессорами AMD64. Это условный ход: если условие выполнено, оно выполняется как mov - иначе оно не действует, за исключением одного: он может прочитать исходный операнд (и, таким образом, может вызвать ошибку из-за доступа к памяти). Вы можете перевернуть условие, которое вы использовали для ветви, вокруг mov, чтобы получить условие для инструкции cmov. (Я сталкивался с в этой теме с участием Линуса Торвальдса , которая указывает, что решение с условным переходом может быть на самом деле лучше или не хуже, чем cmov. Делайте из этого что хотите.)

.loop:
                ; If eax is greater than or equal to dword [rdi], we'll
                ; reassign eax to that dword, the new smallest-yet value.
        cmp eax, dword [rdi]
        cmovge eax, dword [rdi]

                ; Set rdi to point to the next value in the array.
        add rdi, 4
...