1

我是大会的初学者,我有一个简单的问题。这是我的代码:

BITS 64                     ; 64−bit mode
global strchr               ; Export 'strchr'

SECTION .text           ; Code section
strchr:
    mov rcx, -1
.loop:
    inc rcx
    cmp byte [rdi+rcx], 0
    je exit_null
    cmp byte [rdi+rcx], sil
    jne .loop
    mov rax, [rdi+rcx]
    ret

exit_null:
    mov rax, 0
    ret

这个编译但不起作用。如您所见,我想重现函数 strchr 。当我用 printf 测试我的函数时,它崩溃了(问题不在于测试)。我知道我可以直接 INC rdi 进入 rdi 参数并将其返回到我想要的位置。但我只想知道是否有办法在 rcx 位置返回 rdi 来修复我的代码并可能改进它。

4

4 回答 4

3

您的函数strchr似乎需要两个参数:

  1. 指向字符串的指针RDI,和
  2. 指向一个字符的指针RSI

寄存器rcx用作字符串内的索引?在这种情况下,您应该使用al而不是cl. 请注意,您不会限制搜索大小。当RSI在字符串中找不到引用的字符时,很可能会触发异常。也许您应该al在.[rdi+rcx]al=0

如果您希望它返回指向字符串中第一次出现的字符的指针,只需
替换mov rax,[rdi+rcx]lea rax,[rdi+rcx].

于 2022-02-20T18:58:21.290 回答
3

您的代码(来自编辑版本 2)执行以下操作:

char* strchr ( char *p, char x ) {
   int i = -1;
   do {
      if ( p[i] == '\0' ) return null;
      i++;
   } while ( p[i] != x );
   return * (long long*) &(p[i]);
}

正如@vitsoft 所说,您的意图是返回一个指针,但在第一个返回(在汇编中)是返回从找到的字符的地址加载的单个四字,8 个字符而不是地址。


在循环中间递增是不寻常的。从 -1 开始索引也很奇怪。在第一次迭代中,循环继续条件查看p[-1],这不是一个好主意,因为这不是您被要求搜索的字符串的一部分。如果该字节恰好是 nul 字符,它将立即停止搜索。

如果您等到两个测试都执行后才递增,那么您将不会引用 p[-1],并且您也可以从 0 开始索引,这会更常见。


您可能会考虑将字符捕获到寄存器中,而不是三次使用复杂的寻址模式。

此外,您可以将指针推进rdi并完全放弃索引变量。

这是在 C 中的:

char* strchr ( char *p, char x ) {
    for(;;) {
        char c = *p;
        if ( c == '\0' )
            break;
        if ( c == x )
            return p;
        p++;
   }
   return null;
}
于 2022-02-20T19:45:38.220 回答
2

感谢您的帮助,我终于做到了!感谢 Erik 的回答,我修正了一个愚蠢的错误。我正在将 str[-1] 与 NULL 进行比较,所以它出错了。随着 vitsoft 的回答,我将 mov 切换为 lea 并且它起作用了!有我的代码:

strchr:
    mov rcx, -1
.loop:
    inc rcx
    cmp byte [rdi+rcx], 0
    je exit_null
    cmp byte [rdi+rcx], sil
    jne .loop
    lea rax, [rdi+rcx]
    ret

exit_null:
    mov rax, 0
    ret
于 2022-02-20T20:18:27.967 回答
2

当前版本中剩下的唯一错误是加载 8 个字节的 char 数据作为返回值,而不是仅仅进行指针数学运算,使用mov代替lea. (在删除了各种编辑并添加了不同的错误之后,这反映在不同的答案谈论不同的代码)。

但这过于复杂且效率低下(两个加载和索引寻址模式,当然还有额外的指令来设置 RCX)。
只需增加指针,因为这就是您想要返回的内容。

如果您要一次循环 1 个字节,而不是使用 SSE2 一次检查 16 个字节,strchr可以简单如下:

;; BITS 64 is useless unless you're writing a kernel with a mix of 32 and 64-bit code
;; otherwise it only lets you shoot yourself in the foot by putting 64-bit machine code in a 32-bit object file by accident.

global mystrchr
mystrchr:
 .loop:                     ; do {
    movzx  ecx, byte [rdi]   ; c = *p;
    cmp    cl, sil           ; if (c == needle) return p;
    je     .found
    inc    rdi               ; p++
    test   cl, cl
    jnz    .loop            ; }while(c != 0)

      ;; fell out of the loop on hitting the 0 terminator without finding a match
    xor    edi, edi         ; p = NULL
    ; optionally an extra ret here, or just fall through

 .found:
    mov    rax, rdi         ; return p
    ret

我在字符串结尾之前检查了匹配项,因此我仍然拥有未递增的指针,而不必在“找到”返回路径中递减它。如果我用 开始循环inc,我可以使用[rdi - 1]寻址模式,仍然避免使用单独的计数器。这就是为什么我将哪个分支位于循环底部的顺序与问题中的代码进行了切换。

由于我们想将字符比较两次,分别针对 SIL 和零,我将其加载到寄存器中。这可能不会在现代 x86-64 上运行得更快,每个时钟可以运行 2 个负载以及 2 个分支(只要最多使用其中一个)。

一些英特尔 CPU 可以将微融合宏融合 cmp reg,mem / jcc为前端的单个加载+比较和分支微指令,至少在内存寻址模式简单而不是索引时。但不是cmp [mem], imm/ jcc,因此我们不会通过单独加载到寄存器中来为英特尔 CPU 上的前端花费任何额外的微指令。(使用 movzx 来避免写入部分寄存器的错误依赖,例如mov cl, [rdi]


请注意,如果您的调用程序也是用汇编编写的,则返回多个值很容易,例如状态和指针(在未找到的情况下,终止 0 可能会很有用)。 许多 C 标准库字符串函数设计得很糟糕,尤其是strcpy不能帮助调用者避免重做长度查找工作。

特别是在具有 SIMD 的现代 CPU 上,具有显式长度非常有用:现实世界的strchr实现会检查对齐情况,或者检查给定指针是否不在页面末尾的 16 个字节内。但memchr不必这样做,如果大小 >= 16:它可以只movdqu加载和pcmpeqb.

请参阅在 x86 和 x64 上的同一页面内读取缓冲区末尾是否安全?有关详细信息和 glibcstrlen的手写 asm 的链接。还可以使用 simd 查找字符的第一个实例,以用于 glibc 的使用pcmpeqb/等真实世界的实现pmovmskb。(也许pminub对于 0 终止符检查来展开多个向量。)

对于非小字符串,SSE2 可以比此答案中的代码快 16 倍。对于非常的字符串,您可能会遇到内存瓶颈并且“仅”快 8 倍。

于 2022-02-20T22:22:26.323 回答