assembly - 如何遍历汇编中的字符串,直到到达null? (strlen循环)

标签 assembly x86-64 gas att 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”。Superrrrnoob在这里试图学习一些汇编语言。我正在尝试为自己实现strlen函数。

最佳答案

您只需inc %rbx即可增加指针值。 (%rbx)使用其值作为内存地址来取消引用该寄存器。在x86上,每个字节都有其自己的地址(此属性称为“可字节寻址的字节”),而地址只是适合寄存器的整数。

ASCII字符串中的字符全为1字节宽,因此将指针加1将移至ASCII字符串中的下一个字符。 (对于UTF-8且字符在1..127码点范围之外的一般情况,情况并非如此,但是ASCII是UTF-8的子集。)



术语:ASCII码0被称为NUL(一个L),而不是NULL。在C中,NULL是指针概念。 C样式的隐式长度字符串可以描述为0终止或NUL终止,但是“ null终止”滥用了该术语。



您应该选择一个不同的寄存器(称为调用寄存器),这样就无需在函数中推送/弹出该寄存器。您的代码不会进行任何函数调用,因此无需将归纳变量保留在调用保留的寄存器中。

在其他SO Q&A中,我没有找到一个很好的简单示例。它们或者在循环内有2个分支(包括一个无条件的jmp),就像我在注释中链接的那样,或者浪费指令增加一个指针和一个计数器。在循环中使用索引寻址模式并不可怕,但是在某些CPU上效率较低,因此我仍然建议在循环后执行指针增量->减去结束起始。

这就是我写一个最小的strlen的方式,它一次只检查1个字节(缓慢而简单)。我使循环本身保持较小,这是IMO总体上编写循环的一种好方法的合理示例。通常,保持代码紧凑会更容易理解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字节的数组索引=不包括终止符的数据长度。

如果您手动进行内联(因为这只是3条指令的循环),则通常只需要指向0终止符的指针,这样就不会打扰子废话,只需在循环结束时使用RAX。

可以通过剥离第一次迭代来避免在第一次加载之前偏移LEA / INC指令(在第一次cmp之前花费2个周期的等待时间),这可以通过在第一次加载后剥离,或者使用jmp进入cmp / jne处的循环来完成。公司Why are loops always compiled into "do...while" style (tail jump)?

在cmp / jcc之间(如cmp; lea 1(%rax), %rax; jne),用LEA递增指针可能会更糟,因为它会使cmp / jcc的宏融合失败成为单个uop。 (实际上,cmp $imm, (%reg) / jcc的宏融合在像Skylake这样的Intel CPU上都不会发生。cmp微融合内存操作数。也许AMD融合了cmp / jcc。)另外,您将离开RAX 1高于您想要的循环。

因此,对movzx(又名movzbl)加载并将字节零扩展到%ecxtest %ecx, %ecx / jnz中,与循环条件一样有效(在Intel Sandybridge系列上)。但是更大的代码大小。



大多数CPU将在每个时钟周期1次迭代中运行我的循环。通过一些循环展开,我们可能每个周期接近2个字节(尽管仍然仅单独检查每个字节)。

对于大型字符串,一次检查1个字节比使用SSE2慢大约16倍。如果您不打算最小化代码大小和简化代码,请参见Why is this code 6.5x slower with optimizations enabled?以获取使用XMM寄存器的简单SSE2 strlen。 SSE2是x86-64的基线,因此您应该在提速时始终使用它,因为值得在asm中手工编写的内容。



回复:您的更新问题带有来自Why does rax and rdi work the same in this situation?的实现错误的移植

RDI和RBX都保存指针。将它们加在一起不会成为有效地址!在您尝试移植的代码中,RCX(索引)在循环之前被初始化为零。但是您没有使用xor %ebx, %ebx,而是使用了mov %rdi, %rbx。单步执行代码时,请使用调试器检查寄存器值。

关于assembly - 如何遍历汇编中的字符串,直到到达null? (strlen循环),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60482733/

相关文章:

linux - 为什么 gcc 为 x64 共享库强制 PIC?

assembly - vmovdqa 无法在 virtualbox 中工作?

gcc - 内联汇编与C代码混合-如何保护寄存器并最大程度地减少内存访问

assembly - GAS ELF何时需要使用.type,.thumb,.size和.section指令?

c++ - 转储给定 C++ 行的汇编

assembly - 错误 LNK2019 : unresolved external symbol __imp____acrt_iob_func referenced in function _printf in VS2019

assembly - 从 TASM 转换为 NASM

assembly - 如何使用 XACQUIRE、XRELEASE Hardware Lock Elision (HLE) 前缀提示?

visual-c++ - 对于 64 位目标,在 MSVC 2019 中找不到像 _mm_cvtpd_pi32 这样的 MMX 内在函数;与 2013 年相比有何变化?