Как пройти строку в сборке, пока я не достигну нуля? (петля strlen)

Прямо сейчас я только выясняю, как даже пройти по струне. Если код не имеет смысла, это потому, что я неправильно интерпретировал некоторую информацию. В худшем случае я действительно не знаю, что делаю.

strlen:

pushq %rbx
movq %rsi, %rbx


loop:
    cmp $0x00, (%rdi, %rbx)
    je end
    inc %rbx
    jmp loop

end:
    movq %rbx, %rax
    popq %rbx
    ret

PS: Есть причина, по которой мой заголовок выглядит так, как будто старик второй раз на своем компьютере пытается найти "как перейти на google.com" Superrrr noob здесь, пытаясь немного изучить ассемблер. Я пытаюсь реализовать для себя функцию strlen.


person Block o Butter    schedule 02.03.2020    source источник
comment
Проверьте это, это поможет вам понять stackoverflow.com/a/40647017/10927635   -  person vishal    schedule 02.03.2020
comment
cmp с непосредственным операндом требует, чтобы непосредственный операнд был первым (исходным). Кроме того, вы можете просто использовать RDI в качестве указателя в цикле или выбрать регистр с закрытыми вызовами, чтобы вам не приходилось сохранять / восстанавливать RBX.   -  person Peter Cordes    schedule 02.03.2020
comment
Так правильно ли в этом случае использовать% rbx? А как насчет использования% ebx? Я знаю, что в каком-то смысле это просто условность, но для ясности.   -  person Block o Butter    schedule 02.03.2020
comment
Почему rax и rdi работают одинаково в этой ситуации? имеет рабочий strlen в синтаксисе NASM. Это не волшебство, просто увеличьте указатель. И да, RBX подходит inc %rbx, но использование RAX позволит вам делать то же самое без необходимости сохранять / восстанавливать RBX. И нет, вы не хотите использовать 32-битные регистры для указателей.   -  person Peter Cordes    schedule 02.03.2020
comment
Когда я добавляю% rbx, я увеличиваю на 1 байт? Итак, если я сделаю cmp $ 0x00, (% rdi,% rbx), он должен сравнить указатель с нулем, правильно? Насколько я понимаю, в att (% rdi,% rbx) то же самое, что и (rdi + rbx).   -  person Block o Butter    schedule 02.03.2020
comment
RDI и RBX содержат указатели. Их сложение не дает действительного адреса! Я не нашел ни одного SO Q & As с реализацией strlen, которая не отстала бы каким-либо важным образом (например, 2 ветки внутри цикла, как этот), поэтому я написал ответ.   -  person Peter Cordes    schedule 02.03.2020


Ответы (1)


Вы просто inc %rbx увеличиваете значение указателя. (%rbx) разыменовывает этот регистр, используя его значение в качестве адреса памяти. В x86 каждый байт имеет свой собственный адрес (это свойство называется «адресуемым байтом»), а адреса - это просто целые числа, которые помещаются в регистр.

Все символы в строке ASCII имеют ширину 1 байт, поэтому при увеличении указателя на 1 выполняется переход к следующему символу в строке ASCII. (Это неверно в общем случае UTF-8 с символами вне диапазона кодовых точек 1..127, но ASCII является подмножеством UTF-8.)


Терминология: код ASCII 0 называется NUL (один L), а не NULL. В C NULL - это понятие указателя. Строки неявной длины в стиле C могут быть описаны как оканчивающиеся нулем или нулем, но термин «оканчивающийся нулем» неправильно использует терминологию.


Вы должны выбрать другой регистр (тот, который заблокирован вызовом), чтобы вам не нужно было нажимать / вставлять его вокруг вашей функции. Ваш код не выполняет какие-либо вызовы функций, поэтому нет необходимости хранить индукционную переменную в регистре с сохранением вызовов.

Я не нашел хорошего простого примера в других вопросах и ответах SO. У них либо есть 2 ветви внутри цикла (включая один безусловный jmp), как тот, который я связал в комментариях, либо они тратят инструкции на увеличение указателя и счетчика. Использование режима индексированной адресации внутри цикла не страшно, но менее эффективно на некоторых процессорах, поэтому я бы по-прежнему рекомендовал делать приращение указателя -> вычитать конец-начало после цикла.

Вот как я бы написал минимальный strlen, который проверяет только 1 байт за раз (медленно и просто). Я сохранил сам цикл маленьким, и это ИМО - разумный пример хорошего способа написания циклов в целом. Часто сохранение компактности кода упрощает понимание функции в asm. (Дайте ему другое имя, отличное от strlen, чтобы вы могли протестировать его без необходимости gcc -fno-builtin-strlen или чего-то еще.)

.globl simple_strlen
simple_strlen:
    lea     -1(%rdi), %rax     # p = start-1 to counteract the first inc
 .Lloop:                       # do {
    inc     %rax                  # ++p
    cmpb    $0, (%rax)
    jne     .Lloop             # }while(*p != 0);
                           # RAX points at the terminating 0 byte = one-past-end of the real data
    sub     %rdi, %rax     # return length = end - start
    ret

Возвращаемое значение strlen - это индекс массива из 0 байта = длина данных без, включая терминатор.

Если бы вы вставляли это вручную (потому что это всего лишь цикл с тремя инструкциями), вам часто просто нужен указатель на терминатор 0, чтобы вы не беспокоились о вспомогательной ерунде, просто используйте RAX в конце цикла.

Чтобы избежать смещения инструкций LEA / INC перед первой загрузкой (которая стоит 2 цикла задержки перед первым cmp), можно выполнить очистку первой итерации или jmp для входа в цикл на cmp / jne, после inc. Почему циклы всегда компилируются в do ... а стиль (прыжок хвостом)?.

Увеличение указателя с помощью LEA между cmp / jcc (например, cmp; lea 1(%rax), %rax; jne) может быть хуже, потому что оно побеждает макрослияние cmp / jcc в один uop. (На самом деле макро-слияние cmp $imm, (%reg) / jcc в любом случае не происходит на процессорах Intel, таких как Skylake. cmp хотя и выполняет микроплавление операнда памяти. Может быть, AMD объединяет cmp / jcc.) Кроме того, вы оставите цикл с RAX На 1 выше, чем вы хотите.

Таким образом, было бы столь же эффективно (в семействе Intel Sandybridge) movzx (иначе movzbl) загружать и расширять ноль байта до %ecx и test %ecx, %ecx / jnz в качестве условия цикла. Но больший размер кода.


Большинство процессоров будут запускать мой цикл с частотой 1 итерация за такт. Возможно, мы могли бы приблизиться к 2 байтам за цикл (при этом проверяя только каждый байт отдельно) с некоторым развертыванием цикла.

Проверка 1 байта за раз для больших строк примерно в 16 раз медленнее, чем при использовании SSE2. Если вы не стремитесь к минимальному размеру кода и простоте, см. Почему этот код в 6,5 раза медленнее при включенной оптимизации? для простой SSE2 strlen, который использует регистр XMM. SSE2 является базовым для x86-64, поэтому вы всегда должны использовать его, когда он дает ускорение, для вещей, которые стоит писать вручную в asm.


Re: ваш обновленный вопрос с ошибочным портом реализации из Почему rax и rdi работают одинаково в этой ситуации?

RDI и RBX содержат указатели. Их сложение не дает действительного адреса! В коде, который вы пытались перенести, RCX (индекс) инициализируется нулем перед циклом. Но вместо xor %ebx, %ebx вы сделали mov %rdi, %rbx. Используйте отладчик для проверки значений регистров, пока вы выполняете пошаговый код.

person Peter Cordes    schedule 02.03.2020
comment
Спасибо! Ваше объяснение имело смысл. - person Block o Butter; 02.03.2020
comment
@BlockoButter: Ура :) Если это ответ на ваш вопрос, вы можете пометить его как принятый (отметьте галочкой под стрелками вверх / вниз) - person Peter Cordes; 02.03.2020