计算字符串中特定字符的出现次数

Counting the occurrences of a specific char in a string

我正在尝试制作一个执行此任务的混合程序(C++ 和 ASM),但我的 ASM 模块没有按预期工作。文本和字符在程序的 C++ 部分加载。我什至不确定我在哪里犯了错误。

编辑:我使用的是 Borland C++ 和 TASM 编译器 (DosBOX)。该程序要么显示了错误的出现次数(尽管实际出现次数结果相同,但随着我对程序所做的看似无关的更改而发生变化),或者喷出奇怪的字符(如向下箭头符号或笑脸)在应该是数字的地方 - 这是由于在 C++ 中设置了不正确的变量类型引起的。

实际问题肯定是由某些寄存器的值变化引起的,这些寄存器的值显然必须在程序前后保持不变(正如 David Wohlferd 和其他许多人指出的那样 - 谢谢大家)。我查看了导师给我们的文件,根据他们的说法,这些寄存器是(对于 C/C++):DS、SS、SP、BP、SI、DI 和标志寄存器(如果修改了方向标志)在程序中。

这里是更正后的有效代码:https://pastebin.com/JPxMxzmK

.MODEL          SMALL, C
.STACK          400h
.DATA
.CODE
public          CountChar

CountChar   PROC
                push            bp
                mov         bp, sp
                xor         bx, bx
                mov         si, [bp+4]
                mov         ah, [bp+6]

                Check:
                mov         dl, [si]
                cmp         dl, 0
                je          EndOfP
                cmp         dl, ah
                je          Increasing
                inc         si
                jmp         Check

                Increasing:
                inc         bx
                inc         si
                jmp         Check

                EndOfP:
                mov         ax, bx
                pop         bp
                ret

CountChar   ENDP

END

你还没有真正说出哪里出了问题。这使得很难确定 'answer' 可能是什么。但我要尝试一下(嘿,我需要业力)。

阅读您的代码,似乎没有任何实际 "wrong" 代码(尽管有一些我会做不同的事情)。但是,如果要与 C 交互,汇编程序必须遵守一些规则。最重要的规则之一是,如果您更改某些寄存器,您有责任将它们恢复到您找到它们的位置。您的代码违反了这条规则。

作为新手,这对您来说可能有点令人困惑。毕竟,您的 C 代码不会使用寄存器,对吧?除了您的 C 代码 确实 使用寄存器。事实上,这基本上就是 C 编译器的全部目的:将 C 代码(不使用寄存器)转换为汇编代码(使用寄存器)。

如果我们能看到为调用 CountChar 的代码生成的汇编代码,我们会看到 2 个 push 语句(将参数放入堆栈),然后是一个 call CountChar。但是调用代码(可能)使用其他一些寄存器(如 si)来保存其他值。您的 CountChar 例程不能破坏这些值,否则当 CountChar 退出时会发生奇怪的事情。

您可能会问:为什么调用例程在调用您的代码之前不保存所有寄存器的值?它可能。但是每次调用函数时保存所有寄存器(并全部恢复它们)确实会减慢速度。并且您调用的例程完全有可能甚至 使用 所有寄存器,这会浪费时间而没有任何好处。

相反,决定这些事情的人做出了妥协:当调用一个函数时,调用者将假设某些寄存器在函数 returns 时没有改变。具体 which 寄存器可以根据函数的定义方式稍微改变。您可能还没有碰到它,但是有几组规则是代码经常使用的(cdecl、stdcall、pascal、fastcall 等)。

正如 Raymond 所说,对于 16 位代码,cdecl 表示 bp、si 和 di(以及 DS,但我们不会去那里)必须由被调用者保留。当您编写 C 代码时,这一切都已为您完成。但是当你编写汇编程序时,你必须知道(并遵循)这些规则。

这并不意味着您不能使用这些寄存器。只是如果你这样做,你必须在你的函数退出之前保存旧值(例如 push si)并恢复它(例如 pop si)。当然做 push/pop 不是免费的,所以你可能想先使用所有其他寄存器,然后再使用其中一个必须是 saved/restored.

因为这听起来像是一项家庭作业,所以我不会 post 重新编写的代码(反正我没有 运行 它的环境),但是我'我会给你一些建议供你考虑:

  1. 不使用 si(必须保留),而是使用 cx(不保留)。
  2. 不是使用 bx 来保存计数(然后将值移动到 ax),而是首先使用 ax 来保存计数。您可以使用bx来保存您要搜索的字符。
  3. 测试寄存器是否为零时,使用 test dl, dl 比使用 cmp dl, 0.
  4. 快(稍微)快
  5. 查看这段代码:

    cmp         dl, ah
    je          Increasing
    inc         si
    jmp         Check
    
    Increasing:
    inc         bx
    inc         si
    jmp         Check
    

    如果将 inc si 上移到 cmp 指令之前会发生什么情况?那么你就不必把它放在两个地方了:

    inc         si
    cmp         dl, ah
    je          Increasing
    jmp         Check
    
    Increasing:
    inc         bx
    jmp         Check
    

    但是看看发生了什么。现在我们有 2 个紧挨着的跳转指令。不觉得有点多余吗?如果不是在 je 上跳转到 Increasing,而是在 jne 上跳转到 Check,会怎样?现在您的代码如下所示:

    inc         si
    cmp         dl, ah
    jne         Check
    
    inc         bx
    jmp         Check
    
  6. 如果不对注释说点什么,对汇编程序代码的审查是不完整的。这是一小段代码,只是一个练习。但你还是应该养成这个习惯:

    inc         si      ; Position to next byte
    cmp         dl, ah  ; Is this the byte we are counting?
    jne         Check
    
    inc         bx      ; Found one
    jmp         Check
    

    即使像这样的琐碎注释也能使代码方式更容易理解。

    当您从现在起数月(或数年)后返回此代码时,或者当其他人必须拿起您的代码并尝试理解它时(就像今天至少有 3 个人对您的代码所做的那样),它会让生活更轻松。即使(尤其是?)如果代码有误,注释也会显示您的 intent/expectation。

根据您提供的信息,这是我能做出的最佳答案。