試到子程序設計)
簡介匯編語言程序設計是計算機專業(yè)本科階段的基礎(chǔ)實踐課這份武漢理工大學匯編實驗課程報告即圍繞該課程四個實驗整理而成適合正在學習匯編、需要參考實驗報告寫法或調(diào)試記錄的同學使用。內(nèi)容覆蓋熟悉DOSBox與debug開發(fā)調(diào)試環(huán)境、順序程序設計、循環(huán)程序設計、子程序設計四大部分涉及MOV/PUSH、ADD/SUB、MUL/IMUL、DIV/IDIV、DAA以及AND/OR、SHL等指令并記錄了每條指令執(zhí)行后寄存器與內(nèi)存的變化。資源為單個docx文檔壓縮包約172KB小巧便于下載。已有1264人瀏覽學習。報告不僅包含實驗目的、原理與設計步驟還提供詳細的調(diào)試過程、結(jié)果分析和指令功能總結(jié)各實驗均配有完整指令輸入示例與單步調(diào)試記錄可幫助讀者理解CPU寄存器、堆棧、標志位等底層工作機制為后續(xù)匯編編程與實驗報告撰寫提供直觀參考。1. 匯編上機實驗報告從Debug到子程序的完整閉環(huán)這份武漢理工大學匯編實驗課程報告核心不是理論講義而是一份能跟著一步步復現(xiàn)的上機記錄它把匯編語言程序設計這門課拆成了四個必須動手跑通的實驗——用Debug單步觀察指令、順序程序設計、循環(huán)程序設計、子程序設計。適合正在修匯編課需要補實驗報告的人也適合自學8086但卡在DOSBox和Debug環(huán)境里的初學者。拿到這份資源能省掉大量試錯時間環(huán)境怎么搭、指令執(zhí)行后寄存器該是什么值、masm和link怎么調(diào)報告里都留有原始記錄。下面按我的拆解順序把這四個實驗的可復現(xiàn)路徑和暗坑逐一講清楚。2. 用Debug單步觀察四類指令寄存器、棧頂與標志位的真實變化這個實驗是整門課的地基全部在DOSBox 0.74-3里用Debug完成。操作方式是-a輸入機器指令-t單步執(zhí)行每走一步停一次看寄存器。核心價值在于強迫你建立“指令—寄存器—內(nèi)存”的聯(lián)動直覺。Debug里看到的每一個標志位都不是考試死記的而是指令執(zhí)行后硬件真實算出來的結(jié)果。2.1 數(shù)據(jù)傳送指令MOV、PUSH與棧的小端存儲觀察實驗先從棧操作開始輸入代碼如下MOV SP, 00B0 MOV AX, 8057 MOV BX, 0F79 PUSH AX PUSH BX PUSH AX單步執(zhí)行后SP和棧頂?shù)淖兓荘USH AX時SP先減2變成00AE然后把AX8057寫進SS:00AE這個內(nèi)存單元。由于8086是小端存儲低字節(jié)57放在低地址00AE高字節(jié)80放在00AF。接著PUSH BX時SP再次減2到00ACBX0F79低字節(jié)79在00AC高字節(jié)0F在00AD。第三次PUSH AX后SP00AA棧頂又出現(xiàn)57和80。這段代碼的關(guān)鍵不在指令本身而在“先減SP再寫數(shù)據(jù)”這個順序。很多人以為PUSH是把數(shù)據(jù)放到當前SP指向的位置實際上SP變化和寫入是先后兩步。觀測方法是用-d ss:00a0看棧區(qū)連續(xù)字節(jié)再對照SP的值能直接看出低字節(jié)在低地址、高字節(jié)在高地址的排列。報告里還附加了一個MOV WORD PTR [BX]類型的代碼段用WORD PTR指定操作數(shù)寬度這是8086里繞不開的語法細節(jié)——不加PTR編譯器不知道你操作的是字節(jié)還是字。2.2 算術(shù)指令SUB、NEG、MUL、IMUL、DIV、IDIV與標志位聯(lián)動這一組是實驗里信息量最大的部分。以代碼段一為例MOV BX, 9548 MOV CX, 28A7 SUB BX, CX9548h-28A7h6CA1h。結(jié)果非零所以ZF0NZ無符號角度沒有借位所以CF0NC。但用有符號數(shù)眼光看9548h的符號位是1值是-2732028A7h是正數(shù)10407負減正得到-37727超出16位有符號范圍-32768~32767所以OF1OV。結(jié)果6CA1h的最高位為0SF0PL。這個例子非常適合說明“同一組二進制數(shù)據(jù)無符號和有符號視角下標志位含義不同”。MUL和IMUL是一組對照組MOV AL, B4 MOV BL, 11 MUL BL ; 無符號乘法B4h180180×173060轉(zhuǎn)十六進制是0BF4hAX0BF4h。結(jié)果的高8位BFh非零說明低8位裝不下完整乘積因此CFOF1。MOV AL, B4 MOV BL, 11 IMUL BL ; 有符號乘法同樣兩個操作數(shù)在有符號世界里B4h-76-76×17-1292補碼是0FAF4h。AX的值和MUL完全不同但CFOF同樣為1。這里的區(qū)別是看結(jié)果的截斷是否溢出IMUL按有符號規(guī)則檢查高8位FAh不是低8位F4h的符號擴展應該是F4h擴展為FFF4h所以溢出位置1。DIV和IDIV對應展示無符號與有符號除法的商余數(shù)格式MOV AX, 400 MOV BL, B4 DIV BL ; 無符號除法AX0400h即1024BLB4h即1801024÷1805余124商在AL05余數(shù)在AH7C。MOV AX, 400 MOV BL, B4 IDIV BL ; 有符號除法IDIV版本里B4h-761024÷(-76)-13余36。商-13的補碼是0F3h余數(shù)3624h所以ALF3、AH24。注意IDIV的余數(shù)符號和被除數(shù)一致這里被除數(shù)是正數(shù)余數(shù)也為正。如果被除數(shù)是負數(shù)余數(shù)要帶負號這塊容易忽略。2.3 DAA與邏輯移位指令把BCD調(diào)整和位操作看清DAA指令值得單獨講。實驗要求“十進制286896”但直接ADD AL,68的結(jié)果是28h68h90hAL90h并不是BCD意義下的96。緊接著執(zhí)行DAA后AL才變成96h。原理是28h和68h都是BCD編碼每4位表示一位十進制0-9二進制加法里低4位8816產(chǎn)生進位AF1。DAA第一步檢測AF1將AL加06h90h變成96h第二步檢查高4位96h的高4位是9不大于9且CF0不加60h。最終得到正確的BCD結(jié)果96h。MOV AL, 28 ADD AL, 68 ; AL90h DAA ; AL96h邏輯運算和移位實驗驗證位操作的性質(zhì)MOV AL, 48 AND AL, AA ; AL08 MOV BL, 39 OR BL, F0 ; BLF9 MOV AX, 1234 MOV CL, 4 SHL AX, CL ; AX2340AND以AAh為掩碼把AL的0、2、4、6位清零48h與AAh與運算后是08hOR把BL高4位置139h或F0h等于F9h1234h左移4位移出的高4位丟棄、低位補0得到2340h。調(diào)試的重點是每執(zhí)行一條-t停一次看標志位尤其關(guān)注CF和OF——它們是MUL/DIV結(jié)果的擴展位也是實驗報告和考試里最容易填錯的兩個標志。提示DAA的調(diào)整條件是“低4位大于9或AF1”和“高4位大于9或CF1”兩個獨立判斷低位的進位會先觸發(fā)06h調(diào)整再去判斷高位的60h調(diào)整順序不能反。3. 順序程序設計asm源文件到EXE的masm/link/debug四步閉環(huán)從這一章開始進入真正寫源文件、編譯、鏈接、調(diào)試的完整流程。和實驗一直接在Debug里-a輸入不同順序程序必須走完masm → link → debug三步這也是后續(xù)所有實驗的通用上機路徑。3.1 從源代碼到可執(zhí)行文件masm和link的標準調(diào)用順序在DOSBox里先掛載工作目錄mount c d:\asm c: edit source.asm源文件寫完后調(diào)用MASM匯編masm source.asm;分號表示所有后續(xù)提問都用默認項masm會生成source.obj。接著調(diào)用LINKlink source.obj;同樣用分號跳過交互提示得到source.exe。如果不用分號masm會逐個詢問Object Filename、Source Listing、Cross Referencelink會詢問Object Modules、Run File、List File、Libraries——初學者最容易在這里迷路一路按回車也能過去但生成的中間文件會多出lst和map。一個容易被忽略的點源程序必須以end start收尾否則masm會提示缺少結(jié)束偽指令生成的目標文件可能不完整link階段跟著報錯。這是順序程序設計里最常見的報錯源頭比指令寫錯更頻繁。匯編源文件里大小寫不敏感但標號名必須一致start:和end start要對應同一個入口標號。3.2 用Debug跟蹤順序程序-t單步與AX/BX寄存器的變化順序程序?qū)嶒灲o了兩段代碼。第一段與棧相關(guān)mov ax, 2000h mov ss, ax mov sp, 0 add sp, 10 pop ax pop bx push ax push bx pop ax pop bx mov ax, 4c00h int 21h調(diào)試時關(guān)注SS:SP變化軌跡。初始sp0add sp,10后sp10。先執(zhí)行兩次pop把20000h為首地址的棧內(nèi)容彈給AX和BX再把它們壓回去再彈出來。整個過程是入棧出棧的對稱操作所以執(zhí)行到int 21h前AX和BX都回到最初壓棧前的值報告記錄最終(AX)0、(BX)0。跟蹤這類程序時建議用-t跑棧操作并每步記錄一次SP和當前棧頂數(shù)據(jù)。因為棧內(nèi)容始終跟隨SP移動不記錄SP就看不出數(shù)據(jù)流向。Debug里可以用-d ss:0000連續(xù)查看棧區(qū)內(nèi)容對照-r顯示的SP值入棧出棧過程一目了然。3.3 解密程序查表法的地址計算與結(jié)果驗證第二個順序程序是解密程序。密碼表存在數(shù)據(jù)段data segment db 9,0,8,2,7,4,6,3,1,5 data ends題目要求把兩位明碼轉(zhuǎn)成兩位密文例如94→57。查表邏輯是取明碼高位9作為表下標查表[9]5作為密文高位取明碼低位4作為表下標查表[4]7作為密文低位。代碼用si9、di4直接定位mov si, 9 mov di, 4 mov ah, [si] mov al, [di] mov ax, 4c00h int 21h結(jié)果AH5、AL7恰好對應密文57。注意這個寫法是把明碼數(shù)字當作偏移量直接查表不是算法變換。數(shù)據(jù)段里db定義的每個字節(jié)占一個偏移表首地址是ds:0表[9]的偏移就是9。這里有個邊界值得說如果題目要嚴格做“密文→明碼”的反向解密就需要遍歷表找到值等于密文的下標報告里的代碼更接近正向查表輸出密文字符。實際使用中正反兩個方向建議做成兩個子程序避免混用。調(diào)試時在int 21h前用-r查看AX值高位AH和低位AL分別是密文十位和個位。4. 循環(huán)與子程序?qū)崙?zhàn)數(shù)據(jù)搬遷、人均收入計算和show_str顯存操作循環(huán)實驗和子程序?qū)嶒炇沁@份報告里最有分量的部分。前者考驗多段數(shù)據(jù)結(jié)構(gòu)和尋址方式后者考驗棧保護和顯存地址計算都值得花時間仔細拆。4.1 多段結(jié)構(gòu)的循環(huán)程序data、table段與es段超越循環(huán)實驗的數(shù)據(jù)結(jié)構(gòu)定義如下data段連續(xù)存放21個年份字符串每個4字節(jié)、21個收入dword每個4字節(jié)、21個雇員數(shù)word每個2字節(jié)table段預定義21 dup (year summ ne ?? )每項16字節(jié)。區(qū)域偏移范圍單個寬度數(shù)據(jù)內(nèi)容年份0~834字節(jié)1975等ASCII字符串收入84~1674字節(jié)dword型收入數(shù)據(jù)雇員168~2092字節(jié)word型雇員人數(shù)table0~33516字節(jié)年份收入雇員人均收入布局是寫循環(huán)的前提條件。代碼段用es段超越寫入tablemov ax, table mov es, ax mov si, 0 mov di, 0 mov cx, 21 s1: mov ax, [si] mov es:[di], ax mov ax, [si2] mov es:[di2], ax mov ax, [si84] mov es:[di5], ax mov dx, [si842] mov es:[di7], dx add si, 4 add di, 16 loop s1這個循環(huán)里si只負責年份和收入按4字節(jié)步進di按16字節(jié)步進跳到table下一項。第一版很容易犯的錯誤是試圖在一個循環(huán)里同時取雇員數(shù)據(jù)si按4步進算到168時循環(huán)早已超出雇員區(qū)范圍這就是報告里明確記錄的“取雇員人數(shù)時出錯”的翻車現(xiàn)場。正確的做法是把搬運拆成獨立循環(huán)年份和收入用一個循環(huán)雇員數(shù)據(jù)從si168起用另一個循環(huán)按2字節(jié)步進搬運。4.2 人均收入計算與除法溢出邊界人均收入收入/雇員數(shù)。收入是dword需要DX:AX組合成32位被除數(shù)雇員數(shù)是word作為除數(shù)商是16位存入table。代碼分兩段完成mov si, 168 ; 數(shù)據(jù)段雇員區(qū)起點 mov di, 10 ; table第一項雇員數(shù)字段偏移 mov cx, 21 s2: mov ax, [si] mov es:[di], ax add si, 2 add di, 16 loop s2 mov si, 84 ; 數(shù)據(jù)段收入?yún)^(qū)起點 mov di, 168 ; 數(shù)據(jù)段雇員區(qū)起點 mov bx, 13 ; table第一個人均收入字段偏移 mov cx, 21 s3: mov ax, [si] mov dx, [si2] div word ptr [di] mov es:[bx], ax add si, 4 add di, 2 add bx, 16 loop s3div word ptr [di]是典型的32位/16位無符號除法被除數(shù)高位在DX、低位在AX商在AX、余數(shù)在DX不需要管。這里被除數(shù)是收入dword除數(shù)是雇員數(shù)最大年份1995年收入5937000、雇員17800商約33316位范圍內(nèi)不會溢出。但如果把收入數(shù)據(jù)改大DIV會觸發(fā)0號中斷Debug里表現(xiàn)為“Divide overflow”程序直接終止。這個坑實驗里不一定遇到但做擴展題時經(jīng)常冒出來。4.3 子程序show_str行號列號到顯存地址的換算show_str的目標是“在指定位置、用指定顏色顯示0結(jié)尾字符串”。8086文本模式顯存從B8000H開始每行80個字符每個字符占2字節(jié)ASCII碼屬性字節(jié)。因此顯存偏移計算公式是顯存偏移 (行號 × 80 列號) × 2子程序先把行號列號算成偏移再逐字符搬運show_str: push ax push bx push es mov ax, 0B800h mov es, ax mov al, 80 mul dh ; AX 行號 × 80 mov dh, 0 add ax, dx ; 加上列號 add ax, dx ; 列號×2因為每個字符占2字節(jié) mov bx, ax s: mov al, [si] cmp al, 0 je exit mov es:[bx], al mov es:[bx1], cl add bx, 2 inc si jmp short s exit: pop es pop bx pop ax retmul dh是8位無符號乘法DX會被清零但保險起見主程序里最好先mov dh,0。列號偏移加兩次dx等于乘以2避免手寫乘法指令。顏色屬性cl直接寫入每個字符后面的屬性字節(jié)主程序傳0cah這類值時背景色和前景色就由這一個字節(jié)決定。報告里還處理了列號超過79的情況當dl≥80時自動轉(zhuǎn)到下一行對應列這是很多教材示例不處理的邊界但實際調(diào)用者傳錯列號是常事子程序內(nèi)部兜底比調(diào)用端排查省事得多。進入子程序先push ax/bx/es、退出前對稱pop是這里最容易漏但必須養(yǎng)成的習慣后面避坑章還會單獨說。4.4 子程序dtoc除10取余法把數(shù)值轉(zhuǎn)成ASCII字符串dtoc把二進制數(shù)值轉(zhuǎn)成十進制字符串。最簡單可靠的做法是反復除以10依次得到個位、十位、百位把每位加30h轉(zhuǎn)成ASCII字符。除法得到的是逆序所以從字符串尾部往前填或者先壓棧再出棧反轉(zhuǎn)dtoc: push bx push dx mov bx, 10 d: mov dx, 0 div bx ; AX÷10商在AX余數(shù)在DX add dl, 30h ; 數(shù)字→ASCII mov [si], dl dec si cmp ax, 0 jne d pop dx pop bx ret每次div前把DX清零是必須的否則DX里殘留的高位會被當成被除數(shù)的一部分商直接不對。字符串末尾要先放一個0字節(jié)作為結(jié)束符再從后往前填入字符。這個子程序和show_str配合就是“把數(shù)據(jù)用十進制形式顯示”的完整鏈路dtoc負責轉(zhuǎn)換show_str負責上屏。報告里對這兩個子程序的接口設計說得比較清楚——對外只暴露行號、列號、顏色和字符串首地址調(diào)用者不需要知道顯存結(jié)構(gòu)這個思想在后面的子程序里值得延續(xù)。5. 匯編上機避坑五個易翻車的寄存器、除法與尋址細節(jié)下面把四個實驗里最容易讓新手白忙半小時的坑集中列出每條都按“現(xiàn)象→原因→解決”給這些記錄來自報告里的調(diào)試過程和我的復現(xiàn)經(jīng)驗。5.1 現(xiàn)象循環(huán)里取雇員人數(shù)一直錯位寫循環(huán)實驗第一版時搬完年份和收入后在同一個循環(huán)里繼續(xù)取[si168]搬雇員結(jié)果table里雇員數(shù)字和人均收入全亂。原因收入是dword每項4字節(jié)雇員是word每項2字節(jié)同用一個si按4步進第一次循環(huán)si0取雇員區(qū)偏移168沒問題但第二次si4偏移變172實際第二項雇員在170依次錯位。解決把搬運拆成獨立循環(huán)年份收入一個循環(huán)si步長4雇員一個循環(huán)si步長2人均計算再單獨處理。血的教訓是循環(huán)里每一個操作數(shù)的寬度都要單獨驗證步長不能想當然沿用前一個循環(huán)的下標。5.2 現(xiàn)象DIV/IDIV執(zhí)行結(jié)果完全不對有同學直接mov ax, 0400h后執(zhí)行div bl結(jié)果商和余數(shù)看起來像隨機數(shù)。原因DIV的16位被除數(shù)必須放在AX如果被除數(shù)是32位高位必須放到DXDX不作為被除數(shù)一部分時也要顯式清零IDIV還有符號要求負被除數(shù)在IDIV前要做CBWAL符號擴展到AX或CWDAX符號擴展到DX:AX否則DX為0負數(shù)被當成大正數(shù)參與除法。解決寫除法前先明確“被除數(shù)是多少位、有沒有符號”再決定要不要CBW/CWD。實驗一里IDIV BL的B4h是負數(shù)如果漏掉前面對AX的符號處理AL的結(jié)果絕對不是F3。5.3 現(xiàn)象PUSH之后在內(nèi)存里看到的順序和預期相反用-d ss:00a0看棧區(qū)PUSH AX后發(fā)現(xiàn)[00AE]是57、[00AF]是80總覺得“裝反了”。原因8086是小端序低字節(jié)先寫低地址。PUSH AX的完整順序是SP先減2再高字節(jié)進[SS:SP1]、低字節(jié)進[SS:SP]。解決看棧時養(yǎng)成“低地址看低字節(jié)”的習慣必要時用-d看連續(xù)16字節(jié)把寄存器值兩個字節(jié)拼起來對照。這個坑在后續(xù)用棧傳參、保存現(xiàn)場時會反復出現(xiàn)。5.4 現(xiàn)象子程序返回后主程序的變量全變了show_str跑完回到主程序一用AX發(fā)現(xiàn)不是調(diào)用前的值循環(huán)計數(shù)直接亂掉。原因子程序內(nèi)部修改了AX、BX、ES而調(diào)用者默認這些寄存器在call前后保持一致——這是子程序接口的基本約定。解決在子程序入口統(tǒng)一push用到的所有寄存器ret前對稱pop回來。報告里特意在show_str開頭寫“保存子程序中用到的相關(guān)寄存器”因為call/ret只保證指令指針能回來不保證任何寄存器能原樣返回。5.5 現(xiàn)象字符顯示到了奇怪的位置寫show_str時如果把列號直接當偏移加上去字符會跑到屏幕中間偏外或者覆蓋掉不該覆蓋的區(qū)域。原因每列字符實際占2字節(jié)ASCII碼屬性偏移必須按(行×80列)×2計算而不是(行×80列)。解決把公式記成“一列占兩格”先算字符位再乘2。報告里add ax,dx執(zhí)行兩次就是在做列號×2這一步漏掉所有字符都會錯位半個屏幕寬度。6. 上機前先心算結(jié)果用十六進制預判每個實驗的最終值調(diào)試最耗時間的不是寫代碼而是不知道答案對不對。我的習慣是上機前把所有能算的結(jié)果先在草稿上算一遍再開DOSBox驗證。這個習慣幫我躲過了好幾次“代碼跑通但結(jié)果錯”的假成功。以實驗一為例上機前先算MUL BL那一組B4h×11h先轉(zhuǎn)十進制180×173060再轉(zhuǎn)十六進制0BF4h同時知道結(jié)果高字節(jié)非零CFOF1IDIV BL那一組B4h是-761024÷(-76)-13余36商補碼0F3h、余數(shù)24h所以ALF3、AH24。算完再跑用-t單步到指令邊界看到AX0BF4h和標志位CF1時就能確認不是系統(tǒng)在亂跳。順序程序?qū)嶒灷锏臈2僮飨劝裇P推演表畫出來PUSH AX后SP00AE、PUSH BX后SP00AC如果看到SP沒變化說明指令根本沒執(zhí)行到。另一個省時間的驗證手法是用-u反匯編核對輸入。Debug的-a輸入很容易打錯操作數(shù)或?qū)戝e立即數(shù)的進制暫停后用-u看機器碼對應的助記符能立即發(fā)現(xiàn)“原來我寫的是mov ax, b4而不是mov al, b4”。對于循環(huán)21次的長程序沒必要全程單步可以先在int 21h處下斷點用g命令跳過去再用-d查看table段和data段的最終數(shù)據(jù)一次確認整張表寫入是否正確。從那以后我每次帶人做匯編實驗都強制要求先把結(jié)果心算一遍再碰鍵盤這個習慣比任何調(diào)試技巧都管用。希望幫到你。本文還有配套的精品資源點擊獲取