计算字符串中特定字符的出现次数
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 重新编写的代码(反正我没有 运行 它的环境),但是我'我会给你一些建议供你考虑:
- 不使用
si
(必须保留),而是使用 cx
(不保留)。
- 不是使用
bx
来保存计数(然后将值移动到 ax
),而是首先使用 ax
来保存计数。您可以使用bx
来保存您要搜索的字符。
- 测试寄存器是否为零时,使用
test dl, dl
比使用 cmp dl, 0
. 快(稍微)快
查看这段代码:
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
如果不对注释说点什么,对汇编程序代码的审查是不完整的。这是一小段代码,只是一个练习。但你还是应该养成这个习惯:
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。
根据您提供的信息,这是我能做出的最佳答案。
我正在尝试制作一个执行此任务的混合程序(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 重新编写的代码(反正我没有 运行 它的环境),但是我'我会给你一些建议供你考虑:
- 不使用
si
(必须保留),而是使用cx
(不保留)。 - 不是使用
bx
来保存计数(然后将值移动到ax
),而是首先使用ax
来保存计数。您可以使用bx
来保存您要搜索的字符。 - 测试寄存器是否为零时,使用
test dl, dl
比使用cmp dl, 0
. 快(稍微)快
查看这段代码:
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
如果不对注释说点什么,对汇编程序代码的审查是不完整的。这是一小段代码,只是一个练习。但你还是应该养成这个习惯:
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。
根据您提供的信息,这是我能做出的最佳答案。