资讯详情

资讯详情

16位汇编实验5解析:多段程序内存布局与段寄存器初值

先说我个人的看法实验5是整个16位汇编课程里最容易被“误解为简单”的一节。前面几章你还在单一段里写代码数据和指令挤在一起偏移地址靠数数就能算出来到了实验5“段”突然变成了三个数据段、栈段、代码段各管各的。很多同学代码抄对了运行结果也对但是被问一句“为什么加载后DS不在数据段”就卡住了。这篇文章不打算复述一遍答案而是把实验5真正要验证的东西拆开讲清楚从内存布局、段寄存器初值的来龙去脉到一次典型的翻车排查过程都按我实际调试时的顺序写出来。如果你正准备做这个实验或者已经在实验报告上栽过跟头这篇应该能帮上忙。1. 实验5到底在验证什么一个多段程序在内存里怎么“落位”1.1 单段程序与多段程序的本质差异在进入多段程序之前你写的程序一般只有一个代码段需要的数据就顺手定义在代码段里访问的时候用段内偏移直接找。这种做法对几十行的小程序没问题但一旦程序变大数据和代码混在一起就会非常难受你既要记住哪个偏移是变量又要小心别让指令把数据区当成代码执行。实验5把问题从“怎么写几条指令”拉到了“程序怎么被加载进内存”的层面。一个包含数据段、栈段、代码段的程序编译连接成EXE之后DOS加载它时不是简单地把整个文件塞进内存而是按照EXE文件头里的信息把各个段放到它们该放的位置同时设置好一部分段寄存器。这才是实验5最核心的考点你必须说清楚加载之后DS、CS、SS、SP为什么是那些值而不是仅仅会敲汇编指令。从学习节奏上看这也是一个分水岭。前面的程序更像是在一个房间里摆家具所有东西都在同一个空间里实验5开始变成一套房子分了几个房间你进门之后得先找到对应的房间再干对应的事。后面的子程序、中断、端到端程序全部建立在这个“多个段如何协同工作”的基础上。1.2 EXE加载时DS、CS、SS、SP到底谁来定写实验5之前有必要先知道一个事实程序加载完成后寄存器的初值并不是你凭空想象的。对于EXE程序加载过程大致是这样的DS和ES指向程序段前缀PSPPSP是DOS为程序开辟的256字节区域里面保存命令行参数、环境信息等。CS:IP指向程序的入口点这个入口点是end start里的start标号决定的。SS:SP指向栈段的顶部也是栈段最后一个字节后面的位置。数据段的地址虽然被连接器安排好了但不会自动进入任何寄存器必须由你自己的代码把它加载到DS里。很多初学者在Debug里敲R看到DS的值就下意识认为那是数据段地址这是实验5里最常见的理解错误。DS在程序刚加载时其实是PSP段地址你定义的数据段并没有得到任何“默认寄存器”。想要访问数据段必须写上两行mov ax, data mov ds, ax我把加载后的寄存器来源整理了一个表做实验报告时可以对照着写寄存器程序加载后的初值由谁决定DSPSP段地址DOS加载器设置ESPSP段地址DOS加载器设置CS:IP程序入口地址end start指定的入口SS:SP栈段顶部位置EXE文件头中的栈段信息数据段地址不在寄存器里需要代码手动装入DS这里有一个很反直觉的点你的源代码里明明写了assume ds:data为什么DS不自动指向数据段因为这个assume根本不是给CPU看的而是给编译器看的。它告诉编译器代码里没有显式写段前缀的符号访问默认用哪个段寄存器。但段寄存器的实际值必须由mov指令来设置。这两件事的分工就是下一节要展开的内容。2. 动手前必须搞懂的三个底层细节assume、EXE文件头、空栈状态2.1 assume不是执行指令它只是编译器的“上下文声明”assume cs:code, ds:data, ss:stack这三行写在程序最前面看起来像是把段地址装入了寄存器实际上它一个字节的机器码都不会生成。它的作用是告诉编译器code这个段的代码用CS来寻址data段里出现的变量默认用DS来寻址stack段的内容默认用SS来寻址。举个例子。你在数据段定义了一个变量data segment x dw 1234h data ends code segment start: mov ax, x code ends end start如果有了assume ds:data编译器会默认把x翻译成ds:[0]这种寻址方式。但这只是“翻译”层面的约定它并没有把数据段的真实段地址放进DS。真正让DS指向数据段依然需要mov ax, data mov ds, ax第一行把数据段的段地址作为一个16位立即数送入AX第二行再送入DS。8086不允许直接把立即数送入段寄存器所以必须经过通用寄存器中转。这个限制本身也是实验5里值得写进报告的知识点。我见过不少同学在完成实验时觉得反正有assume ds:data于是直接mov ax, [bx]访问数据结果读出来的是PSP里的内容调试半天才发现DS没设置。记住assume是写给人看、写给编译器看的执行流里的段地址全部要靠指令自己搬。2.2 EXE文件头里的CS、IP、SS、SP与内存重定位实验5要求你把程序编译成EXE再放进Debug里调试这不是为了走过场而是因为EXE的加载方式和COM不同。COM文件是个朴素的内存镜像加载地址固定CS、DS、ES、SS四个段寄存器全都指向同一个段EXE文件则不同它有一个文件头文件头里记录了入口点、栈段位置以及一张重定位表。这里可以简单理解成编译器生成的目标文件里所有段地址都不是写死的实际物理地址而是一个个“待填项”。连接器把这些段排好序并且在EXE文件头里记下哪些地方需要在加载时修正。DOS把程序加载到内存后会根据实际加载到的起始地址把入口点、栈段等信息修正成真实地址再设置CS:IP和SS:SP。你在Debug里看到的CS、SS已经是修正后的结果。这也是为什么同一个EXE在DOS的不同位置加载显示出的段地址可能不同但程序都能正常运行。调试时不要纠结“为什么CS是这样一个数”而应该关注“它和DS、SS之间的关系”。数据段的段地址会被写入到mov ax, data这条指令的立即数位置由加载器修正。Debug里执行到这条指令后AX里就会显示一个可用的段地址这就是后面用D命令查看数据段内容的依据。2.3 “空栈”状态到底长什么样实验5里的程序往往要压栈、出栈于是栈段怎么初始化就成了必考项。很多人对“空栈”的理解是SP等于0。这个理解放到8086上是不对的。8086的压栈操作是先把SP减2再把数据写入SP指向的地址。也就是说一个16字节的栈段如果定义成stack segment dw 0, 0, 0, 0, 0, 0, 0, 0 stack ends栈段一共16个字节当你把SP设为16十六进制就是10h表示栈顶在栈段末尾之后的一个字位置。此时执行第一次pushSP会先变成140Eh然后把数据写到栈段偏移14的位置。随着不断压栈SP一路向下走直到用完整个栈段。如果SP被错误地设为0第一次push就会先把SP变成0FFFCh再往SS:FFFC处写入数据。这个地址早就跑出栈段范围了可能落在内存里任何位置轻则数据错乱重则把其他程序的数据盖掉。我建议你在实验报告里写清楚一组对应关系栈段定义了多少字节SP的初值就是多少。如果想偷懒用16进制也要先换算明白。这个细节在后面的排查章节还会再出现。3. 完整流程从编写源代码到用Debug验证结果3.1 一份典型的三段式模板代码实验5最常见的题目形态是数据段里定义若干个字利用栈把这些字逆序存放回数据段。我用的模板代码如下assume cs:code, ds:data, ss:stack data segment dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h data ends stack segment dw 0, 0, 0, 0, 0, 0, 0, 0 stack ends code segment start: mov ax, data mov ds, ax mov ax, stack mov ss, ax mov sp, 10h mov bx, 0 mov cx, 8 s: push ds:[bx] add bx, 2 loop s mov bx, 0 mov cx, 8 s2: pop ds:[bx] add bx, 2 loop s2 mov ax, 4c00h int 21h code ends end start这段代码的逻辑是第一次循环把数据段偏移0、2、4……一直到14的8个字依次压入栈第二次循环再把它们弹回数据段的相同偏移位置。因为栈是后进先出的所以最后数据段里原来的顺序会变成逆序。初始是0123h, 0456h, ……, 0987h运行后应当变成0987h, 0cbah, 0fedh, 0defh, 0abch, 0789h, 0456h, 0123h。注意几个设计点数据是字类型所以每次偏移加2循环次数是8用CX控制push ds:[bx]里的ds:一定要有因为这时DS已经通过前面的两条指令指向数据段了。最后的mov ax, 4c00h和int 21h是返回DOS的标准写法不能漏。3.2 用MASM工具链生成EXE源代码写好后用配套的汇编器和连接器处理。常用的命令是masm E5.asm;分号表示后面的提示都用默认回答会生成E5.obj文件。然后再执行link E5.obj;生成E5.exe。如果编译报错先检查段定义有没有写错、标点是不是英文状态下的、end start是不是写在最后一行。连接时如果提示栈相关的问题多半是栈段定义有问题回到源码检查栈段是否真的存在。这一步最重要的是理解为什么需要两步汇编器把源文件翻译成目标文件此时段之间的相对位置还没确定连接器负责把各个段组织成最终的EXE并生成文件头。你要调试的“多段程序”其实是连接器排好序之后的结果。实验报告里如果只写了源代码没写汇编、连接过程等于只走了一半。3.3 Debug里的完整验证顺序生成EXE后在Debug里加载debug E5.exe先执行R记录寄存器初值。此时DS指向PSPCS指向程序入口所在的代码段SS指向栈段SP值为10h。注意这里的SP已经是加载器根据EXE文件头设置好的不需要你手动算。接着用T单步执行第一条mov ax, data。执行完这条后AX里会出现数据段地址。然后继续T执行mov ds, ax。走到这一步DS才会真正变成数据段地址。此时可以用D命令查看数据段内容命令格式是D 数据段段址:0对照偏移0到15应该能看到初始的8个字。继续单步到压栈循环每压一个数可以顺便用R看一下SP的变化会发现SP每次减2。压完8个后用D SS:0查看栈段栈段里应该保存着数据段的8个字顺序是反的。再继续单步到弹栈循环最后再看数据段此时数据已经逆序。需要注意执行到int 21h之前最后一条指令不要用T单步进去否则会进入DOS中断内部很难出来。对int 21h这条用P让它直接返回程序正常结束回到Debug提示符。我用表格整理一下验证点操作命令期望看到的现象查看初始寄存器RDS指向PSPCS:IP指向入口SS:SP指向栈顶执行mov ax,dataTAX变成数据段段地址执行mov ds,axTDS变成数据段段地址查看数据段初值D 段址:08个字按源码顺序排列单步压栈过程TSP每次减2栈段内容逐步增多查看栈段D SS:0数据被逆序保存在栈段单步弹栈过程TSP每次加2数据段逐步变化查看最终数据段D 段址:08个字变成倒序结束程序P回到Debug提示符4. 一次“代码看起来没问题数据却全乱掉”的排查过程4.1 现象栈段比预想中小一号后面的数据全错有一回我帮同学调实验5代码逻辑看起来完全正常DS设置了循环次数对了压栈和弹栈也是对称的。但是程序跑完后数据段前4个字是对的后4个字变成了莫名其妙的值。用D命令查看数据段前8个字节还能看出规律后面全是乱码。最后定位到的原因就一行mov sp, 8这个同学认为栈段定义了8个字所以SP就应该是8。实际上栈段8个字占16个字节SP应当是16也就是10h。他设成8之后第一次压栈SP变6第二次变4第三次变2第四次变0第五次压栈时SP直接溢出到0FFFEn数据写到了栈段之外。所以前4个压栈的字是正常的后4个字写到别处去了之后再弹出数据段前半段看着没问题后半段读到的是栈段外的垃圾值。这种问题在源码静态检查时特别不容易发现因为指令看着都是对的只有把执行过程拆开看SP的变化才能看出来。4.2 用逐步观察寄存器的方式定位根因排查过程其实不复杂先在压栈循环前设置断点用R看SP初值确认是不是16然后每压一个数执行一次T同步看SP的值。正确的变化序列应该是16 - 14 - 12 - 10 - 8 - 6 - 4 - 2 - 0而错误的版本从第五次开始变成了16 - 14 - 12 - 10 - 8 - 6 - 4 - 2 - 0 - FFFE - FFFC ...看到SP变成FFFE基本就不用再怀疑别的了问题一定出在SP初值比栈段实际容量小。再回头检查栈段定义和SP初始化的对应关系很快就能锁定代码。这个坑在教材的课后题里也经常被拿来考给你一段代码让你指出为什么执行后数据不正确。答案往往就是SP初始化和栈段长度不匹配。如果你只是背答案不亲手调一次永远感受不到SP溢出是个什么场景。4.3 同一类翻车DS没有真正指向数据段另一类非常类似的问题是assume写得好好的但程序里漏掉了对DS的赋值。比如记忆里总觉得ambience as ifassume ds:data已经完成了使命于是循环里直接写push [bx]由于DS在程序加载时指向PSP[bx]读到的就是PSP里的内容根本不是数据段定义的数据。Debug里检查时可以先执行到第一次push之前用R看DS的值再和之前mov ax, data后AX的值对比如果对不上说明DS没被正确赋值。解决方法是把两行赋值补上mov ax, data mov ds, ax这类问题的共同特征是只看源码“能不能看懂”看不出毛病必须把寄存器的实际值和代码意图对照起来才能找到真正的根因。实验5训练的就是这个能力。5. 实验报告与自测中最值得写的几个验证点5.1 加载后先别急着运行记录寄存器初值实验报告的第一个亮点应该是加载完成、还没执行任何指令时用R记录下来的寄存器状态。别小看这一步它能直观证明你知道哪些寄存器是由DOS加载器设置的。DS指向PSP、CS:IP指向入口、SS:SP指向栈顶这三个结论如果能在报告里写出来并用Debug截图佐证那实验5的核心部分就达标了。还有一种考法是把end start改成end问你程序还会不会从start开始执行。答案是缺了入口标号IP会被设为代码段偏移0处。如果start在代码段的最开始程序确实能跑如果start前还有其他数据或指令程序就会从错误位置开始执行。这个点很适合写进实验报告体现对入口地址的理解。5.2 用D命令证明数据段发生了“逆序变化”运行模板程序后数据段应该是从正序变成逆序。验证时不能只看程序“正常返回”一定要用D命令把数据段最终内容列出来再和初始内容做对比。报告里可以画一张简单的对照表左边是原始8个字右边是运行后的8个字。这样老师一眼就能看出你确实理解栈的后进先出特性。如果做的是压栈再弹回原位置的变体那最终数据段不变但中间过程里栈段会发生变化。无论哪种变体都要抓住一个本质栈段的修改是临时的数据段是最终验证对象。5.3 观察SS:SP在压栈和弹栈过程中的变化规律实验5的另一个隐含考点是SS:SP在压栈时每次减2弹栈时每次加2。很多同学把栈的“向下增长”挂在嘴边却没有真正用R观察过SP的变化。在Debug里压一次栈看一次记录一行SP的值报告里放10行左右的变化序列比任何文字说明都有说服力。这里要注意的是Debug里T单步执行mov ss, ax时有些环境下下一条指令会立刻执行这是8086处理器的特性修改SS之后会暂时屏蔽中断并把下一条指令也一起执行完。遇到这种情况不要慌跟着SP的值判断就行不要误以为Debug坏了。5.4 用自检清单尽量避免低级错误最后分享一个我长期使用的自检清单实验五代码写完提交前对照一遍能消掉绝大多数低级错误检查项正确做法出错表现SP初值等于栈段字节数栈溢出数据段被无关内存覆盖DS赋值mov ax,datamov ds,ax访问到PSP内容数据错乱循环次数CX等于数据字个数多循环或少循环结果不对偏移步长字操作每次加2数据被拆成字节处理程序返回mov ax,4c00hint 21h程序无法正常结束入口指定end startIP可能指向代码段偏移0处个人经验是这六项里最容易出错的是SP初值其次是DS赋值。现在很多集成开发环境会自动处理初始化但教材实验让你用Debug手动调就是希望你能掌握寄存器和段地址之间的真实关系。环境替你代劳的部分恰恰是最需要自己亲手确认的部分。实验5真正的收获不是你跑通了一个程序而是你终于能解释清楚程序在内存里的每一步变化了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →