Начало этого ответа основано на том, что прокомментировал Шут. Это просто расширяет это и объясняет изменения более подробно. Я также внес некоторые дополнительные изменения, два из которых также исправляют ошибки в ваших источниках.
Во-первых, эта часть:
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