【问题标题】:Assembly loop through a string to count characters汇编循环遍历字符串以计算字符数
【发布时间】:2016-11-15 19:09:40
【问题描述】:

我尝试编写一个汇编代码来计算字符串中有多少个字符,但出现错误。

代码,我使用 gcc 和 intel_syntax

#include <stdio.h>

int main(){
char *s = "aqr  b qabxx xryc pqr";
int x;

asm volatile (
    ".intel_syntax noprefix;"
    "mov eax, %1;"
    "xor ebx,ebx;"
    "loop:"
        "mov al,[eax];"
        "or al, al;"
        "jz print;"
        "inc ebx;"
        "jmp loop"
    "print:"
    "mov %0, ebx;"
    ".att_syntax prefix;"
    : "=r" (x)
    : "r" (s)
    : "eax", "ebx"
);

    printf("Length of string: %d\n", x);
    return 0;

}

我得到了错误:

Error: invalid use of register

最后我想制作一个程序,它搜索正则表达式模式([pq][^a]+a)并打印它的起始位置和长度。我用 C 写的,但我必须让它在汇编中工作: 我的 C 代码:

#include <stdio.h>
#include <string.h>

int main(){
  char *s = "aqr  b qabxx xryc pqr";
  int y,i;
  int x=-1,length=0, pos = 0;

    int len = strlen(s);
    for(i=0; i<len;i++){
        if((s[i] == 'p' || s[i] == 'q') && length<=0){
            pos = i;
            length++;
            continue;
        } else if((s[i] != 'a')) && pos>0){
            length++;
        } else if((s[i] == 'a') && pos>0){
            length++;
            if(y < length) {
                y=length;
                length = 0;
                x = pos;
                pos = 0;    
            }
            else 
                length = 0;
                pos = 0;
        }
    }  

    printf("position: %d, length: %d", x, y);
    return 0;

}

【问题讨论】:

  • 有错误的行号吗?
  • 你不应该在编译器后面切换汇编语法,否则它会做错误的替换。如果您需要 intel 语法,请使用 -masm=intel 编译器开关。
  • 7 行,它开始组装块
  • @Jester 这是我教授的指令,我只能在 "mov eax, %1" 和 "print:" 之间编写代码
  • 那你的教授不称职。 jmp loop 之后还缺少一个分号。

标签: c string gcc assembly intel-syntax


【解决方案1】:

您省略了jmp loopprint: 之后的分号。


你的 asm 也不能正常工作。将指向s 的指针移动到eax 中,然后用mov al,[eax] 覆盖它。所以下一次通过循环,eax 不再指向字符串。

当你解决这个问题时,你需要考虑这样一个事实,即每次通过循环都需要将 eax 更改为指向下一个字符,否则 mov al,[eax] 会继续读取相同的字符。


由于您尚未接受答案(通过单击左侧的复选标记),因此还有时间再进行一次编辑。

通常我不“做别人的功课”,但已经有几天了。估计作业的截止日期已经过去了。在这种情况下,这里有一些解决方案,既适用于 OP 的教育,也适用于未来的 SO 用户:

1) 遵循作业的(有些奇怪的)限制:

asm volatile (
    ".intel_syntax noprefix;"
    "mov eax, %1;"
    "xor ebx,ebx;"
    "cmp byte ptr[eax], 0;"
    "jz print;"
    "loop:"
        "inc ebx;"
        "inc eax;"
        "cmp byte ptr[eax], 0;"
        "jnz loop;"
    "print:"
    "mov %0, ebx;"
    ".att_syntax prefix;"
    : "=r" (x)
    : "r" (s)
    : "eax", "ebx"
);

2) 违反一些赋值规则来制作稍微好一点的代码:

asm (
    "\n.intel_syntax noprefix\n\t"
    "mov eax, %1\n\t"
    "xor %0,%0\n\t"
    "cmp byte ptr[eax], 0\n\t"
    "jz print\n"
    "loop:\n\t"
        "inc %0\n\t"
        "inc eax\n\t"
        "cmp byte ptr[eax], 0\n\t"
        "jnz loop\n"
    "print:\n"
    ".att_syntax prefix"
    : "=r" (x)
    : "r" (s)
    : "eax", "cc", "memory"
);

这减少了 1 个寄存器(没有 ebx)并省略了(不必要的)volatile 限定符。它还添加了“cc”clobber 以指示代码修改了标志,并使用“memory”clobber 来确保在执行 asm 之前对s 的任何“待处理”写入都被刷新到内存中。它还使用格式 (\n\t),因此使用 -S 构建的输出是可读的。

3) 使用更少寄存器的高级版本(没有eax),检查以确保s 不为NULL(返回-1),使用符号名称并假定-masm=intel 导致更易读的代码:

__asm__ (
    "test %[string], %[string]\n\t"
    "jz print\n"
    "loop:\n\t"
        "inc %[length]\n\t"
        "cmp byte ptr[%[string] + %[length]], 0\n\t"
        "jnz loop\n"
    "print:"
    : [length] "=r" (x)
    : [string] "r" (s), "[length]" (-1)
    : "cc", "memory"
);

摆脱(任意且未经过深思熟虑的)分配约束允许我们将其减少到 7 行(如果我们不检查 NULL,则为 5 行,如果我们不计算标签则为 3 [实际上不是说明])。

还有一些方法可以进一步改进这一点(在标签上使用 %= 以避免可能出现的重复符号问题,使用本地标签 (.L),甚至编写它以使其适用于两者 -masm=intel-masm=att 等),但我敢说这三个中的任何一个都比原始问题中的代码更好。


好吧,Kuba,在你接受答案之前,我不知道你还有什么需要。不过,它确实让我有机会包含 Peter 的版本。

4) 指针增量:

__asm__ (
    "cmp byte ptr[%[string]], 0\n\t"
    "jz .Lprint%=\n"
    ".Loop%=:\n\t"
    "inc %[length]\n\t"
    "cmp byte ptr[%[length]], 0\n\t"
    "jnz .Loop%=\n"
    ".Lprint%=:\n\t"    
    "sub %[length], %[string]"
    : [length] "=&r" (x)
    : [string] "r" (s), "[length]" (s)
    : "cc", "memory"
);

这不会执行 #3 中的“空指针”检查,但会执行 Peter 推荐的“指针增量”。它还避免了潜在的重复符号(使用 %=),并使用“本地”标签(以 .L 开头的标签)以避免将额外符号写入目标文件。

从“性能”的角度来看,这可能会稍微好一些(我没有计时)。然而,从“学校项目”的角度来看,#3 的清晰性似乎是一个更好的选择。从“如果出于某种奇怪的原因我不得不在 asm 中而不是仅仅使用标准的 c 函数来编写这个,我会在现实世界中写什么”的角度来看,我可能会看看用法,除非这对性能至关重要,我很想选择 #3 以方便将来的维护。

【讨论】:

  • 我实际上更喜欢指针增量版本(并非所有英特尔 CPU 都可以微融合索引寻址模式),但不要在循环中增加计数器,只需执行 len = end - start找到终点。
  • @PeterCordes - 我试着用 c 写这个来找出哪个 gcc 会生成。大概这将是“最好的”。不幸的是,它会生成两者,具体取决于 c 代码的编写方式。
  • 只要循环小于 3 ALU 微指令,无论哪种方式都没有多大关系。前端不应该成为瓶颈,因此优化微融合是无关紧要的。 (并且不要假设 gcc 知道这一点。它并不完美,而且我不认为它对不同寻址模式的成本的权重是一个模型,特别是因为它在这方面的行为并没有太大不同-mtune=nehalem vs. -mtune=sandybridge vs. -mtune=skylake(SKL 可以再次在 OOO 内核中微融合索引寻址模式,而不仅仅是在解码器/uop 缓存中)
  • 虽然我不认为 gcc 是最佳性能的权威,但对于我们这些还没有准备好阅读 Agner Fog 的作品来形成初步意见的人来说,这是一种可靠的方式。我的期望是,大多数“大”胜利往往都在那里。您只需要准备好重新考虑现实世界的时间是否显示其他情况。
  • 是的,这绝对是一个好方法。 gcc 通常会避免遇到任何严重的停顿,尽管它有时会导致本可以很容易避免的部分寄存器停顿,即使使用-mtune=core2。 ://
猜你喜欢
  • 2023-03-23
  • 2016-01-24
  • 2019-09-14
  • 2017-10-06
  • 1970-01-01
  • 1970-01-01
  • 2020-02-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多