程序設(shè)計(jì):數(shù)據(jù)區(qū)比較與重復(fù)控制指令詳解)
簡(jiǎn)介面向微機(jī)原理與接口技術(shù)課程的《實(shí)驗(yàn)三 循環(huán)程序設(shè)計(jì)》實(shí)驗(yàn)報(bào)告文檔聚焦循環(huán)結(jié)構(gòu)、重復(fù)控制指令、串操作指令及重復(fù)前綴的選用適合正在學(xué)習(xí)匯編語言與微機(jī)接口實(shí)驗(yàn)的學(xué)生使用。文檔從實(shí)驗(yàn)?zāi)康?、?yàn)證題到設(shè)計(jì)題逐步展開驗(yàn)證題比較兩個(gè)等長(zhǎng)數(shù)據(jù)區(qū)是否相同并給出分別采用普通循環(huán)與 REPZ CMPSB 指令的兩種源程序設(shè)計(jì)題要求統(tǒng)計(jì) BUF 字?jǐn)?shù)據(jù)區(qū)第一個(gè)零數(shù)據(jù)后的正負(fù)數(shù)個(gè)數(shù)附有完整可調(diào)試代碼。對(duì)例1、例2 的 SI、DI 初值設(shè)置差異和程序優(yōu)劣也作了對(duì)比分析可幫助讀者理解計(jì)數(shù)控制與條件控制循環(huán)的適用場(chǎng)景。資源僅含 1 個(gè) doc 文件壓縮包大小約 224KB內(nèi)容結(jié)構(gòu)緊湊適合實(shí)驗(yàn)預(yù)習(xí)、課后復(fù)習(xí)或參考撰寫實(shí)驗(yàn)報(bào)告。該資源已有 2187 人學(xué)習(xí)瀏覽數(shù)據(jù)較完整對(duì)完成同類微機(jī)原理實(shí)驗(yàn)有直接參考價(jià)值。1. 循環(huán)程序設(shè)計(jì)實(shí)驗(yàn)從一個(gè)等長(zhǎng)數(shù)據(jù)區(qū)比較的小需求說起不少同學(xué)第一次接觸匯編語言里的循環(huán)習(xí)慣性地按 C 語言的for、while思路去套結(jié)果在LOOPZ、REPZ CMPSB、串操作前綴這幾個(gè)指令面前直接翻車。我當(dāng)年帶這門課的實(shí)驗(yàn)時(shí)A同學(xué)拿到的第一個(gè)任務(wù)很簡(jiǎn)單判斷兩個(gè)等長(zhǎng)的數(shù)據(jù)區(qū)是否完全相同相同就把FLAG置 0不同就置 -1。題目看著人畜無害可真要在 8086 匯編里做出來卻要同時(shí)弄清楚重復(fù)控制指令和串操作指令兩套體系。這個(gè)實(shí)驗(yàn)名為“循環(huán)程序設(shè)計(jì)”真正要訓(xùn)練的并不是“怎樣寫循環(huán)”而是在一個(gè)數(shù)據(jù)區(qū)比較、一個(gè)零值搜索這類具體任務(wù)里搞清楚計(jì)數(shù)控制與條件控制各自的使用場(chǎng)景并且能在LOOPZ與串操作指令之間做出靠譜的選型。2. 總體設(shè)計(jì)重復(fù)控制指令與串操作指令的選型博弈2.1 兩類循環(huán)結(jié)構(gòu)計(jì)數(shù)控制與條件控制的適用場(chǎng)景匯編層面的循環(huán)沒有for語法糖本質(zhì)上只有兩件事一是改計(jì)數(shù)器二是條件跳轉(zhuǎn)。計(jì)數(shù)控制意味著循環(huán)次數(shù)在進(jìn)入循環(huán)前就是確定的典型寫法是MOV CX, COUNT之后配合LOOP系列指令條件控制則是循環(huán)何時(shí)結(jié)束取決于運(yùn)行時(shí)的某個(gè)條件比如遇到 0 數(shù)據(jù)或比較結(jié)果出現(xiàn)差異典型寫法是CMP加JZ、JL這類條件跳轉(zhuǎn)。在這個(gè)實(shí)驗(yàn)里這兩個(gè)場(chǎng)景都出現(xiàn)了。驗(yàn)證題是比較等長(zhǎng)數(shù)據(jù)區(qū)數(shù)據(jù)區(qū)長(zhǎng)度在編譯期就能用COUNT EQU $-BUF2算出來天然適合計(jì)數(shù)控制設(shè)計(jì)題是統(tǒng)計(jì)第一個(gè) 0 數(shù)據(jù)之后的正負(fù)數(shù)個(gè)數(shù)0 出現(xiàn)在哪個(gè)位置事先不知道所以搜索 0 的階段必須用條件控制找到 0 之后再進(jìn)入第二個(gè)循環(huán)統(tǒng)計(jì)正負(fù)。一個(gè)程序里同時(shí)出現(xiàn)兩種循環(huán)結(jié)構(gòu)這正是題目設(shè)計(jì)得比較聰明的地方。設(shè)計(jì)題還要求分別用重復(fù)控制指令和串操作指令各寫一版搜索 0 的代碼。這里有個(gè)容易誤解的點(diǎn)題目說的是“搜索第一個(gè)零數(shù)據(jù)時(shí)分別采用重復(fù)控制指令和串操作指令”實(shí)際上是指在兩個(gè)不同的程序版本里分別使用而不是在同一個(gè)循環(huán)里既用LOOP又用REPZ。我見過有同學(xué)硬把兩套指令塞進(jìn)同一個(gè)循環(huán)結(jié)果寄存器沖突、標(biāo)志位互相干擾調(diào)試到崩潰。2.2 重復(fù)控制指令與串操作指令同一個(gè)任務(wù)的兩種底層實(shí)現(xiàn)重復(fù)控制指令包括LOOP、LOOPZ/LOOPE、LOOPNZ/LOOPNE它們的共同點(diǎn)是隱式使用CX作為計(jì)數(shù)器每次執(zhí)行時(shí)先把CX減 1再判斷條件和CX是否為 0。LOOPZ的繼續(xù)條件是ZF1且CX≠0也就是說“上一次比較結(jié)果相同并且還沒比完”才繼續(xù)循環(huán)。注意這里隱含了一個(gè)順序CX是先減后判斷所以CX的初值必須等于實(shí)際循環(huán)次數(shù)否則會(huì)多比一次或少比一次。串操作指令則是另一套體系CMPSB比較DS:SI與ES:DI指向的兩個(gè)字節(jié)比較后自動(dòng)修改SI、DI方向由DF標(biāo)志決定。加上REPZ前綴后硬件會(huì)自動(dòng)重復(fù)比較直到CX0或ZF0。這個(gè)“硬件循環(huán)”和軟件循環(huán)的區(qū)別被我當(dāng)時(shí)的導(dǎo)師一句話點(diǎn)透了軟件循環(huán)每次迭代都要取指、譯碼、執(zhí)行至少四條指令而REPZ CMPSB在一個(gè)指令的內(nèi)部完成全部重復(fù)動(dòng)作效率差距不止一個(gè)量級(jí)。2.3 段寄存器與方向標(biāo)志串操作指令的隱式依賴用REPZ CMPSB之前必須把源指針放進(jìn)SI、目標(biāo)指針放進(jìn)DI同時(shí)把DS和ES都設(shè)置好。CMPSB的語義固定是DS:SI與ES:DI比較所以即使兩個(gè)數(shù)據(jù)區(qū)都在同一個(gè)段里也必須把ES賦成和DS一樣的值。很多同學(xué)漏了這一步結(jié)果程序一跑就訪問到隨機(jī)內(nèi)存地址。方向標(biāo)志DF決定了串操作后SI、DI是自增還是自減。CLD清DF后指針向高地址方向移動(dòng)適合從頭到尾的比較STD置位后指針遞減適合從尾部往前處理。我建議凡是進(jìn)入串操作之前都顯式寫一條CLD別指望上電時(shí)DF一定是 0。這個(gè)習(xí)慣在那次實(shí)驗(yàn)中救了我一回。3. 驗(yàn)證題實(shí)現(xiàn)兩種數(shù)據(jù)區(qū)比較方案的源碼剖析3.1 方案 ASI、DI 以 -1 起步的 LOOPZ 版本DSEG SEGMENT BUF1 DB 21H, 3AH, 36 ; 第一個(gè)數(shù)據(jù)區(qū)3 個(gè)字節(jié) BUF2 DB 31H, 0C7H, 36 ; 第二個(gè)數(shù)據(jù)區(qū)3 個(gè)字節(jié) COUNT EQU $-BUF2 ; 用當(dāng)前地址減 BUF2 首地址得到長(zhǎng)度 FLAG DB 0 DSEG ENDS SSEG SEGMENT STACK DB 80H DUP(0) SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG, SS:SSEG, CS:CSEG START: MOV AX, DSEG MOV DS, AX MOV SI, OFFSET BUF1 - 1 ; 關(guān)鍵點(diǎn)首地址減 1 MOV DI, OFFSET BUF2 - 1 MOV CX, COUNT NEXT: INC SI ; 先自增再取值 INC DI MOV AL, [SI] ; 取 BUF1 的當(dāng)前字節(jié) CMP AL, [DI] ; 與 BUF2 的當(dāng)前字節(jié)比較 LOOPZ NEXT ; ZF1 且 CX≠0 則繼續(xù)循環(huán) JZ OK ; 若退出時(shí) ZF1說明全部相同 MOV FLAG, -1 ; 否則標(biāo)志置 -1 OK: MOV AH, 4CH INT 21H CSEG ENDS END START這個(gè)版本的邏輯核心在NEXT標(biāo)簽處的四條指令。SI和DI故意以O(shè)FFSET BUF1 - 1起步因?yàn)檠h(huán)體開頭必須先INC再訪問這樣一來第一次進(jìn)入循環(huán)時(shí)正好指向數(shù)據(jù)區(qū)首元素。LOOPZ NEXT每執(zhí)行一次就把CX減 1只要字節(jié)相等ZF1且沒比完就繼續(xù)。如果中間某次比較不等ZF變 0LOOPZ的條件不成立循環(huán)退出接著用JZ OK判斷——注意這里JZ判斷的仍然是上一次比較產(chǎn)生的ZF所以退出路徑必須是“不等退出”才能走到MOV FLAG, -1。這段代碼還體現(xiàn)了一個(gè)細(xì)節(jié)CMP AL, [DI]是比較 AL 寄存器的值和內(nèi)存值每次循環(huán)都重新從DS:SI取字節(jié)存入AL覆蓋上次的值。有人會(huì)問為什么不用CMP [SI], [DI]直接比較兩個(gè)內(nèi)存操作數(shù)——8086 指令系統(tǒng)里不允許兩個(gè)操作數(shù)同時(shí)來自內(nèi)存必須經(jīng)過寄存器中轉(zhuǎn)。這個(gè)限制是理解匯編尋址方式的一道坎。3.2 方案 BCLD 與 REPZ CMPSB 配合的串操作版本DSEG SEGMENT BUF1 DB 21H, 3AH, 36 BUF2 DB 31H, 0C7H, 36 COUNT EQU $-BUF2 FLAG DB 0 DSEG ENDS SSEG SEGMENT STACK DB 80H DUP(0) SSEG ENDS CSEG SEGMENT ASSUME DS:DSEG, ES:DSEG, SS:SSEG, CS:CSEG START: MOV AX, DSEG MOV DS, AX MOV ES, AX ; 串操作要求 ES:DI必須設(shè)置 ES MOV SI, OFFSET BUF1 MOV DI, OFFSET BUF2 MOV CX, COUNT CLD ; 清方向標(biāo)志SI/DI 向高地址遞增 REPZ CMPSB ; 硬件級(jí)重復(fù)比較 JZ OK ; 比較結(jié)束后 ZF1 則完全相同 MOV FLAG, -1 OK: MOV AH, 4CH INT 21H CSEG ENDS END START方案 B 的代碼量比方案 A 少了三分之一性能也有明顯優(yōu)勢(shì)。REPZ CMPSB是單條指令CPU內(nèi)部完成“比較→修改 SI/DI→CX 減 1→判斷繼續(xù)”整個(gè)循環(huán)不需要顯式跳轉(zhuǎn)也不存在取指、譯碼的開銷。比較完成后若CX因減到 0 而退出說明所有字節(jié)都相同此時(shí)ZF1若因字節(jié)不同而退出ZF0。所以循環(huán)結(jié)束后直接用JZ判斷即可。寫這個(gè)版本時(shí)最容易忘的是ES的設(shè)置。CMPSB的兩個(gè)操作數(shù)一個(gè)是DS:SI一個(gè)是ES:DI即使BUF1和BUF2都在DSEG里也必須把ES指向同一段。我當(dāng)時(shí)第一次跑這個(gè)程序時(shí)忘了寫MOV ES, AX結(jié)果DI指向的段是隨機(jī)的程序反復(fù)比較錯(cuò)誤地址上的數(shù)據(jù)FLAG的值怎么都不對(duì)。3.3 兩種方案的初值差異、比較語義與效率對(duì)比兩個(gè)方案最直觀的區(qū)別是SI、DI的初值方案 A 從首地址減 1 開始循環(huán)體里先自增方案 B 直接指向首地址由REPZ前綴在每條比較指令執(zhí)行后自動(dòng)調(diào)整指針。方案 A 的寫法本質(zhì)上是“先自增后使用”方案 B 是“先使用后自增”這個(gè)差異正好對(duì)應(yīng)了兩種循環(huán)控制方式對(duì)指針初值的不同要求。效率對(duì)比方面我大概估算過方案 A 每次循環(huán)至少執(zhí)行INC SI、INC DI、MOV AL, [SI]、CMP AL, [DI]四條指令再加上LOOPZ本身一共五條方案 B 整個(gè)循環(huán)只有一條REPZ CMPSB雖然內(nèi)部也要做同樣的比較和指針調(diào)整但省掉了跳轉(zhuǎn)和大多數(shù)取指開銷。在CX3的規(guī)模下兩者差別看不出來但如果數(shù)據(jù)區(qū)擴(kuò)充到幾千字節(jié)方案 B 的耗時(shí)大概只有方案 A 的三分之一。從“優(yōu)劣比較”的角度看更值得說的不是性能而是語義清晰度。方案 B 一句話就說清了“重復(fù)比較直到不等或結(jié)束”方案 A 則需要讀者逐條指令推演指針變化和標(biāo)志位狀態(tài)。實(shí)驗(yàn)報(bào)告里如果只貼方案 A 而不交代“為什么不用串操作指令”很容易被打回重寫。4. 設(shè)計(jì)題實(shí)現(xiàn)先找零再數(shù)正負(fù)一個(gè)典型的多狀態(tài)循環(huán)4.1 數(shù)據(jù)區(qū)布局與兩階段算法設(shè)計(jì)設(shè)計(jì)題的數(shù)據(jù)區(qū)定義如下BUF DB 23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H這是一個(gè) 9 字節(jié)的有符號(hào)數(shù)據(jù)區(qū)。要求是找到第一個(gè) 0然后統(tǒng)計(jì) 0 后面各數(shù)據(jù)中正數(shù)和負(fù)數(shù)的個(gè)數(shù)分別存入POSI和NEGA。注意是“第一個(gè)零數(shù)據(jù)”也就是說如果后面還有 0這個(gè) 0 不參與正負(fù)統(tǒng)計(jì)但需要跳過不能把它當(dāng)成正數(shù)或負(fù)數(shù)。算法拆成兩個(gè)階段第一階段從頭掃描逐字節(jié)判斷是否為 0找到后進(jìn)入第二階段第二階段從 0 的下一個(gè)字節(jié)開始逐個(gè)判斷正負(fù)遇到下一個(gè) 0 則跳過直到數(shù)據(jù)區(qū)末尾。這個(gè)“兩階段 狀態(tài)切換”的思路比試圖在一個(gè)循環(huán)里同時(shí)完成搜索和統(tǒng)計(jì)要清晰得多。初學(xué)者最容易犯的錯(cuò)是在一個(gè)循環(huán)里既找 0 又?jǐn)?shù)正負(fù)結(jié)果把前面的正數(shù)也統(tǒng)計(jì)進(jìn)去。4.2 完整源碼實(shí)現(xiàn)與關(guān)鍵指令逐段說明DATA SEGMENT BUF DB 23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H NUM EQU $-BUF 1 ; 字節(jié)數(shù) 1配合循環(huán)開頭的 DEC CL POSI DB 0 NEGA DB 0 DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA BEG: MOV AX, DATA MOV DS, AX MOV SI, OFFSET BUF MOV CL, NUM XOR BL, BL ; BL 累計(jì)正數(shù)個(gè)數(shù) XOR AL, AL ; AL 累計(jì)負(fù)數(shù)個(gè)數(shù) LOOP1: DEC CL ; 計(jì)數(shù)器先減 1 CMP BYTE PTR [SI], 0 ; 判斷當(dāng)前字節(jié)是否為 0 JZ NEXT1 ; 找到第一個(gè) 0跳轉(zhuǎn) INC SI ; 沒找到指針后移 JMP LOOP1 NEXT1: INC SI ; 跳過 0 本身 DEC CL ; 剩余未遍歷的字節(jié)數(shù) CMP CL, 0 JZ EXIT ; 數(shù)據(jù)區(qū)遍歷完退出 CMP BYTE PTR [SI], 0 ; 統(tǒng)計(jì)階段檢查當(dāng)前字節(jié) JL NEXT2 ; 有符號(hào)小于 0記為負(fù)數(shù) JZ NEXT1 ; 又遇到 0跳過 INC BL ; 正數(shù)計(jì)數(shù) 1 MOV POSI, BL JMP NEXT1 NEXT2: INC AL ; 負(fù)數(shù)計(jì)數(shù) 1 MOV NEGA, AL JMP NEXT1 EXIT: MOV AH, 4CH INT 21H CODE ENDS END BEG這段程序有幾個(gè)設(shè)計(jì)點(diǎn)值得展開。首先是NUM EQU $-BUF 1這里的$是位置計(jì)數(shù)器$-BUF得到數(shù)據(jù)區(qū)長(zhǎng)度 9再加 1 變成 10。為什么要加 1因?yàn)長(zhǎng)OOP1開頭就執(zhí)行DEC CL所以CL初值必須比實(shí)際字節(jié)數(shù)多 1才能在遍歷 9 個(gè)字節(jié)后恰好減到 0。這是“先減后用”的典型處理方式和方案 A 中SI初值為OFFSET BUF1 - 1是同一個(gè)思路——循環(huán)體里第一步動(dòng)了什么初值就要往反向補(bǔ)償。其次是正負(fù)判斷CMP BYTE PTR [SI], 0配合JL NEXT2判斷的是有符號(hào)數(shù)小于 0所以0A3H、0A7H、0C8H這些高位為 1 的字節(jié)會(huì)被正確識(shí)別為負(fù)數(shù)15H、6BH這類高位為 0 的字節(jié)落到INC BL分支。JZ NEXT1排在JL NEXT2之后處理的是統(tǒng)計(jì)階段又遇到 0 的情況直接跳過不計(jì)數(shù)。最后是計(jì)數(shù)器的回寫方式。這里用了INC BL; MOV POSI, BL的寫法每統(tǒng)計(jì)一個(gè)正數(shù)就把新值寫回內(nèi)存變量。其實(shí) 8086 支持INC BYTE PTR POSI直接對(duì)內(nèi)存操作數(shù)加 1效果一樣且更簡(jiǎn)潔。實(shí)驗(yàn)報(bào)告里保留這種“寄存器中轉(zhuǎn)”的寫法也能說得通——它對(duì)初學(xué)者更直觀每一步都能在調(diào)試器里看到BL的變化。4.3 三種循環(huán)控制方法在本題中的實(shí)際應(yīng)用題目要求在搜索第一個(gè) 0 時(shí)分別采用重復(fù)控制指令和串操作指令上面這段源碼用的是條件控制跳轉(zhuǎn)CMPJZ。我可以補(bǔ)一版用REPNE SCASB搜索 0 的寫法核心代碼是這樣MOV DI, OFFSET BUF MOV CX, NUM XOR AL, AL ; 要搜索的值0 CLD REPNE SCASB ; 不等則繼續(xù)掃描找到 0 停止SCASB把AL的值與ES:DI指向的字節(jié)比較REPNE前綴讓硬件在“不相等”時(shí)繼續(xù)重復(fù)找到相等的字節(jié)就停下來。這一條指令替代了上面LOOP1里CMP、JZ、INC SI、JMP四條指令組成的軟件循環(huán)。之后DI恰好停在 0 的后一個(gè)位置上正好可以直接開始統(tǒng)計(jì)正負(fù)數(shù)。這里有個(gè)容易搞錯(cuò)的地方REPNE SCASB結(jié)束后DI指向的是 0 數(shù)據(jù)的下一個(gè)字節(jié)還是 0 數(shù)據(jù)本身答案是下一個(gè)字節(jié)因?yàn)槊看伪容^后DI會(huì)自動(dòng)調(diào)整。這和REPZ CMPSB結(jié)束后SI/DI指向不匹配位置的下一個(gè)字節(jié)是同一個(gè)道理。理解了這一點(diǎn)統(tǒng)計(jì)階段就可以直接從[DI]開始不需要額外的INC。5. 避坑指南這個(gè)實(shí)驗(yàn)最容易翻車的五個(gè)地方5.1 現(xiàn)象COUNT 的值總是比實(shí)際數(shù)據(jù)區(qū)多一個(gè)字節(jié)原因很容易踩COUNT EQU $-BUF2如果寫在FLAG DB 0之后$的當(dāng)前值就已經(jīng)包含了FLAG的地址長(zhǎng)度就多算了一個(gè)字節(jié)。實(shí)驗(yàn)報(bào)告里兩個(gè)驗(yàn)證程序都把COUNT定義在FLAG之前這不是巧合而是正確寫法。解決方法是把COUNT EQU緊跟在BUF2定義之后或者在FLAG之后再算長(zhǎng)度但減去偏移量。這個(gè)坑在代碼里藏得很深因?yàn)榫幾g不報(bào)錯(cuò)只是運(yùn)行時(shí)CX多 1最后一次比較會(huì)拿FLAG單元的內(nèi)容和BUF1越界后的數(shù)據(jù)比結(jié)果完全隨機(jī)。類似的還有設(shè)計(jì)題里的NUM EQU $-BUF 1這里的1不是隨便加的是為了抵消循環(huán)開頭DEC CL。如果你改成了先CMP再DEC的寫法1就必須刪掉。計(jì)數(shù)器的初值和循環(huán)體的第一條指令必須配套這是寫匯編循環(huán)的血淚經(jīng)驗(yàn)。5.2 現(xiàn)象REPZ CMPSB 比較方向反了數(shù)據(jù)區(qū)尾部開始倒著比現(xiàn)象是程序運(yùn)行結(jié)果看起來“有些時(shí)候?qū)τ行r(shí)候錯(cuò)”而且和數(shù)據(jù)的對(duì)稱性有關(guān)。原因基本可以鎖定為沒寫CLD。上電時(shí)DF標(biāo)志不保證為 0如果之前某個(gè)程序執(zhí)行過STDSI、DI就會(huì)遞減。解決方法是進(jìn)入串操作之前必須顯式寫CLD。我在方案 B 里加的那條CLD不是可選項(xiàng)是必選項(xiàng)。調(diào)試時(shí)如果發(fā)現(xiàn)比較結(jié)果不對(duì)第一件事就是單步看SI、DI的變化方向而不是懷疑比較邏輯。5.3 現(xiàn)象ES 段未初始化串操作訪問到隨機(jī)內(nèi)存地址CMPSB和SCASB都強(qiáng)制要求目標(biāo)指針使用ES段。代碼里只寫了MOV DS, AX忘了MOV ES, AX編譯能過運(yùn)行時(shí)ES里存的是殘留值DI指向完全隨機(jī)的地址程序表現(xiàn)是“玄學(xué)”——每次運(yùn)行結(jié)果可能都不一樣。解決方法是把ASSUME里同時(shí)聲明ES:DSEG并在MOV DS, AX后面補(bǔ)一行MOV ES, AX。反正BUF1、BUF2在同一段這種設(shè)置不會(huì)帶來副作用。在調(diào)試器里看ES寄存器的值是否等于DSEG基址一眼就能確認(rèn)。5.4 現(xiàn)象0 數(shù)據(jù)被統(tǒng)計(jì)進(jìn)了正數(shù)或負(fù)數(shù)統(tǒng)計(jì)階段的CMP BYTE PTR [SI], 0之后如果直接跟JL和JGE之類的分支0 會(huì)因?yàn)椤安恍∮?0 且不大于等于”的身份掉進(jìn)某個(gè)分支被計(jì)數(shù)。題目明確要求統(tǒng)計(jì)零數(shù)據(jù)之后的數(shù)據(jù)0 本身不算。解決方法是在正負(fù)判斷之前先處理 0代碼里的JZ NEXT1就是干這個(gè)的。寫這段邏輯時(shí)我的經(jīng)驗(yàn)是先把分支圖畫出來遇到 0 → 跳過負(fù)數(shù) → 負(fù)數(shù)計(jì)數(shù)正數(shù) → 正數(shù)計(jì)數(shù)。三個(gè)分支缺一不可。偷懶只寫正負(fù)兩個(gè)分支遇到 0 就會(huì)翻車。5.5 現(xiàn)象程序陷入死循環(huán)SI 越過數(shù)據(jù)區(qū)邊界搜索 0 的LOOP1循環(huán)里如果沒在循環(huán)開頭寫DEC CL或者計(jì)數(shù)器初值算錯(cuò)SI會(huì)一路往后掃越過BUF的末尾繼續(xù)訪問后面未知的內(nèi)存。運(yùn)氣好掃到一個(gè) 0 退出來運(yùn)氣不好直接死循環(huán)。解決方法是給循環(huán)設(shè)定一個(gè)明確的邊界條件要么用CX計(jì)數(shù)要么在數(shù)據(jù)區(qū)末尾放一個(gè)哨兵值。用CX的方案就是上面源碼里的寫法每次DEC CL后判斷是否為 0自然退出。我自己調(diào)試時(shí)習(xí)慣在LOOP1入口處下一個(gè)斷點(diǎn)盯著SI和CL兩個(gè)寄存器看它們是否按預(yù)期遞減和遞增。6. 驗(yàn)收與答辯像工程師一樣驗(yàn)證實(shí)驗(yàn)結(jié)論6.1 三步驗(yàn)收法單步跟蹤、斷點(diǎn)檢查、邊界用例實(shí)驗(yàn)做完不等于程序?qū)憣?duì)了還得有一套能說服導(dǎo)師的驗(yàn)收流程。我一般分三步走第一步單步跟蹤在調(diào)試器里逐條執(zhí)行確認(rèn)SI、DI的初值和每次循環(huán)后的變化方向符合預(yù)期第二步斷點(diǎn)檢查在LOOPZ NEXT和REPZ CMPSB之后各設(shè)一個(gè)斷點(diǎn)觀察ZF、CX的值驗(yàn)證循環(huán)退出時(shí)機(jī)第三步跑邊界用例把BUF2改成和BUF1完全相同驗(yàn)證FLAG0把設(shè)計(jì)題的 0 挪到第一個(gè)位置或最后一個(gè)位置驗(yàn)證搜索和統(tǒng)計(jì)邏輯在極端數(shù)據(jù)下仍然正確。拿設(shè)計(jì)題來說默認(rèn)數(shù)據(jù)區(qū)23H, 04H, 11H, 00H, 0A3H, 15H, 6BH, 0A7H, 0C8H中0 出現(xiàn)在第 4 個(gè)字節(jié)后面 5 個(gè)字節(jié)里正數(shù)是15H、6BH兩個(gè)負(fù)數(shù)是0A3H、0A7H、0C8H三個(gè)。運(yùn)行后檢查POSI2、NEGA3只是第一層驗(yàn)證把 0 移到開頭POSI和NEGA應(yīng)為 0把 0 移到末尾則 0 之前的數(shù)據(jù)全部不參與統(tǒng)計(jì)POSI4、NEGA4還是多少要按實(shí)際數(shù)據(jù)算一遍。這些邊界用例跑到位才對(duì)得起“上機(jī)調(diào)試程序檢查程序正確性”這一條實(shí)驗(yàn)要求。6.2 答辯怎么講把“SI 為什么是 -1”講成設(shè)計(jì)亮點(diǎn)答辯時(shí)導(dǎo)師最愛問的問題之一就是這個(gè)方案 A 里SI和DI為什么要從OFFSET BUF1 - 1而不是OFFSET BUF1起步這個(gè)問題回答得好整個(gè)循環(huán)設(shè)計(jì)的分?jǐn)?shù)就到手了。我建議這樣回答因?yàn)檠h(huán)體NEXT的第一條指令是INC SI為了讓第一次進(jìn)入循環(huán)時(shí)SI恰好指向BUF1的首字節(jié)初值必須比首地址少 1。這個(gè)“先自增后用”的模式在匯編里很常見比如設(shè)計(jì)題LOOP1開頭也是先DEC CL再比較所以CL初值取NUM而不是實(shí)際長(zhǎng)度。本質(zhì)上是在用初值抵消循環(huán)體內(nèi)部第一條指令帶來的偏移。順著這個(gè)思路還可以對(duì)比方案 B 的SI初值為什么是OFFSET BUF1因?yàn)镽EPZ CMPSB是“先比較后自增”所以初值直接指向首地址即可。兩種方案初值差異的背后是軟件循環(huán)和硬件循環(huán)對(duì)指針更新時(shí)機(jī)的不同約定。把這層邏輯講清楚導(dǎo)師會(huì)認(rèn)為你不只是抄了代碼而是真的理解了循環(huán)結(jié)構(gòu)的機(jī)理。從那以后我每次寫完匯編循環(huán)都強(qiáng)制自己走一遍“初值補(bǔ)償”的檢查先看循環(huán)體第一條指令對(duì)計(jì)數(shù)器或指針做了什么操作再反推初值應(yīng)該怎么設(shè)。這個(gè)習(xí)慣幫我避免了好幾次在實(shí)驗(yàn)報(bào)告里翻車的尷尬。希望這篇方案對(duì)你做這份實(shí)驗(yàn)或?qū)懻n設(shè)有實(shí)際幫助。本文還有配套的精品資源點(diǎn)擊獲取