MASM 使用 Irvine32 读取并解析文件时仅输出 +67
结论
问题不在文件路径,代码中至少有三处错误会破坏解析或遍历结果:
ParseTempsFromString的两个参数压栈顺序写反了。- 被调过程已经通过
RET 8和RET 4清理参数,调用方又执行了ADD ESP,造成栈指针失衡。 WriteTempsReverse用ECX保存循环下标,却在循环中调用WriteInt和输出宏。调用结束后,不能假定ECX仍保留原值。
+67 并不是从文件中正确解析出的温度,更可能来自错误地址、损坏的数组内容或被改写的寄存器。由于源码并不完整,宏定义也被省略,无法准确追溯 67 的具体来源。不过,当前代码的调用约定和寄存器管理确实有误。
参数压栈顺序写反了
过程按下面的方式读取参数:
MOV ESI, [EBP + 8] ; fileBuffer
MOV EDI, [EBP + 12] ; tempArray
x86 参数按从右到左的顺序压栈。这个过程可以理解为:
ParseTempsFromString(fileBuffer, tempArray)
调用时应先压入第二个参数,再压入第一个参数:
PUSH OFFSET tempArray
PUSH OFFSET readBuffer
CALL ParseTempsFromString
原代码的顺序正好相反:
PUSH OFFSET readBuffer
PUSH OFFSET tempArray
CALL ParseTempsFromString
进入过程后,寄存器实际得到的是:
ESI = OFFSET tempArray
EDI = OFFSET readBuffer
这样一来,解析器会把初始化为零的 tempArray 当成字符串。数组的第一个字节就是 0,所以程序通常会立即跳到 ParseEnd,不会解析文件内容。
如果实际运行时仍然输出了一个数,可能是编译运行的代码与贴出的代码不完全一致,也可能还有栈、宏或寄存器方面的问题。
参数被清理了两次
ParseTempsFromString 末尾使用:
RET 8
WriteTempsReverse 末尾使用:
RET 4
这两个过程会自行移除参数,采用的是 stdcall 风格。调用方不能再执行:
ADD ESP, 8
ADD ESP, 4
否则每组参数都会被清理两次。调用代码应改为:
PUSH OFFSET tempArray
PUSH OFFSET readBuffer
CALL ParseTempsFromString ; RET 8 已清理参数
PUSH OFFSET tempArray
CALL WriteTempsReverse ; RET 4 已清理参数
调用约定要保持一致,可以选择以下任意一种:
- 被调过程使用
RET n,调用方不调整ESP。 - 被调过程使用普通
RET,调用方通过ADD ESP, n清理参数。
两种方式不能混用。
ECX 不适合跨库调用保存循环下标
WriteTempsReverse 使用 ECX 保存当前数组下标:
MOV ECX, [numTempsParsed]
DEC ECX
循环中又调用了:
CALL WriteInt
mDisplayChar DELIMITER
除非相关过程或宏明确保证会保留 ECX,否则调用结束后应认为 EAX、ECX 和 EDX 都可能发生变化。如果第一次输出后 ECX 被改成 0,接下来的 DEC ECX 会把它变成 -1,循环也就只执行一次。
可以在输出前后显式保存下标:
WriteTempsReverse PROC STDCALL
PUSH EBP
MOV EBP, ESP
PUSH ESI
MOV ESI, [EBP + 8]
MOV ECX, [numTempsParsed]
TEST ECX, ECX
JZ WriteEnd
DEC ECX
WriteLoop:
MOV EAX, [ESI + ECX*4]
; 输出过程可能修改 ECX,因此显式保存
PUSH ECX
CALL WriteInt
mDisplayChar DELIMITER
POP ECX
DEC ECX
JNS WriteLoop
WriteEnd:
CALL CrLf
POP ESI
POP EBP
RET 4
WriteTempsReverse ENDP
解析循环可以不调用 IsDigit
IsDigit 本身没有问题,但解析器同时使用多个寄存器保存数组下标、符号和累加值。在循环中调用外部过程,会增加寄存器被修改的风险。
这里直接比较字符范围更简单:
CMP AL, '0'
JB NotDigit
CMP AL, '9'
JA NotDigit
下面的解析过程与当前数据格式相符:
ParseTempsFromString PROC STDCALL
PUSH EBP
MOV EBP, ESP
PUSH ESI
PUSH EDI
PUSH EBX
MOV ESI, [EBP + 8] ; readBuffer
MOV EDI, [EBP + 12] ; tempArray
XOR ECX, ECX ; 已解析的元素数量
ParseLoop:
SkipSeparators:
MOV AL, [ESI]
CMP AL, 0
JE ParseEnd
CMP AL, DELIMITER
JE SkipOne
CMP AL, ' '
JE SkipOne
CMP AL, 0Dh
JE SkipOne
CMP AL, 0Ah
JE SkipOne
JMP ReadSign
SkipOne:
INC ESI
JMP SkipSeparators
ReadSign:
XOR EDX, EDX ; 0 = 正数,1 = 负数
CMP BYTE PTR [ESI], '-'
JNE CheckPlusSign
MOV EDX, 1
INC ESI
JMP RequireDigit
CheckPlusSign:
CMP BYTE PTR [ESI], '+'
JNE RequireDigit
INC ESI
RequireDigit:
MOV AL, [ESI]
CMP AL, '0'
JB InvalidCharacter
CMP AL, '9'
JA InvalidCharacter
XOR EBX, EBX ; 数值累加器
ReadDigits:
MOV AL, [ESI]
CMP AL, '0'
JB StoreNumber
CMP AL, '9'
JA StoreNumber
SUB AL, '0'
MOVZX EAX, AL
IMUL EBX, EBX, 10
ADD EBX, EAX
INC ESI
JMP ReadDigits
StoreNumber:
MOV EAX, EBX
TEST EDX, EDX
JZ SaveNumber
NEG EAX
SaveNumber:
MOV [EDI + ECX*4], EAX
INC ECX
CMP ECX, TEMPS_PER_DAY
JB ParseLoop
JMP ParseEnd
InvalidCharacter:
; 防止遇到异常字符时停在同一位置反复存储 0
CMP BYTE PTR [ESI], 0
JE ParseEnd
INC ESI
JMP ParseLoop
ParseEnd:
MOV [numTempsParsed], ECX
POP EBX
POP EDI
POP ESI
POP EBP
RET 8
ParseTempsFromString ENDP
对应的调用方式是:
PUSH OFFSET tempArray
PUSH OFFSET readBuffer
CALL ParseTempsFromString
文件缓冲区还有一个边界问题
当前代码最多允许读取 bufferSize 个字节:
MOV ECX, bufferSize
CALL ReadFromFile
随后又在 readBuffer + bytesRead 处写入结尾的 0:
MOV BYTE PTR [EBX], 0
如果恰好读取了 1024 字节,这个 0 就会写到缓冲区之外。读取时应为字符串结束符预留一个字节:
MOV EAX, fileHandle
MOV EDX, OFFSET readBuffer
MOV ECX, bufferSize - 1
CALL ReadFromFile
JC FileReadErrorHandler
MOV bytesRead, EAX
MOV BYTE PTR [readBuffer + EAX], 0
示例文件很短,这通常不会直接产生当前的 +67,但这个边界问题仍需修正。
修正后的关键调用代码
; 读取文件
MOV EAX, fileHandle
MOV EDX, OFFSET readBuffer
MOV ECX, bufferSize - 1
CALL ReadFromFile
JC FileReadErrorHandler
MOV bytesRead, EAX
MOV BYTE PTR [readBuffer + EAX], 0
; ParseTempsFromString(readBuffer, tempArray)
PUSH OFFSET tempArray
PUSH OFFSET readBuffer
CALL ParseTempsFromString
; 不要 ADD ESP, 8
; 输出
mDisplayString OFFSET successMsg
CALL CrLf
PUSH OFFSET tempArray
CALL WriteTempsReverse
; 不要 ADD ESP, 4
调试顺序
修正调用约定后,可以在调试器中依次检查:
ReadFromFile返回后,EAX应保存实际读取的字节数。readBuffer开头应是"-3,-2,0,3"对应的 ASCII 字节。- 进入
ParseTempsFromString后:[EBP+8]应等于OFFSET readBuffer。[EBP+12]应等于OFFSET tempArray。
- 解析完成后,
numTempsParsed应为24。 tempArray[0]应为-3,tempArray[23]应为-1。- 每次调用
WriteInt后,确认循环下标已经通过栈或其他方式得到保护。
修正参数顺序、栈清理和 ECX 保存方式后,这组输入应按数组下标从 23 到 0 输出完整的反向序列。
备注:内容仅供参考。
