资讯详情

资讯详情

汇编核心:8086寄存器分类、物理地址与标志位详解

1. 这一章到底在解决什么问题寄存器为什么非学不可1.1 寄存器是CPU的工作台不是存储仓库很多朋友第一次翻开汇编语言教材看到寄存器三个字就开始头疼因为它既不是能摸到的器件也不是像变量那样直白的抽象概念。我当年学的时候也是这样总觉得这一章就是一张要背的表格直到后来做底层调试才真正理解寄存器是CPU内部一组只有几个字节大小的高速存储单元它之所以重要是因为CPU指令不能直接操作硬盘上的文件也不能直接操作内存里的任意字节绝大多数指令只能先操作寄存器里的数据再把结果写回内存。用一个小类比来解释内存像冰箱硬盘像库房寄存器则是灶台上的案板和调料架。厨师炒菜时不会每道菜都跑一趟库房而是把正在用的食材和调料摆在手边。CPU也是这样它把当前正在计算的中间结果放进寄存器下一步运算随时取用速度比内存快好几个量级。这些寄存器合起来有14个每个都是16位经典8086架构下后面所有指令、寻址、栈、中断的知识点都建立在这14个名字之上。学不会这一章后面的mov、add、call、loop全是浮云学透了这一章你会突然发现汇编的大部分语法其实就是在说把哪个寄存器的内容送到哪里去。1.2 这一章结束前你应该能回答四个问题我把这一章的学习目标归纳成四句话读者可以拿来自测能不看表说出经典8086架构下14个寄存器的名字并给它们分对类。能理解AX、BX、CX、DX各自有什么约定俗成的用途而不是只当成四个一模一样的桶。能算出一个逻辑地址对应的物理地址并解释为什么一个物理地址会有多种写法。知道标志寄存器里那些位分别叫什么、什么情况下会被置1。这四个目标覆盖了本章的绝大多数考点和面试常问题。如果合上书能回答上来这一章就算真正过关了。2. 16位CPU的寄存器版图通用、段、指针三大体系2.1 一张表看清14个寄存器先上总表。经典8086架构下所有寄存器都是16位。分类寄存器中文习惯叫法主要用途数据/通用寄存器AX累加器算术运算核心乘除法的默认操作数数据/通用寄存器BX基址寄存器可作偏移地址进行内存寻址常用于查表数据/通用寄存器CX计数寄存器loop循环计数、移位次数等数据/通用寄存器DX数据寄存器乘除法扩展位、I/O端口号变址/通用寄存器SI源变址寄存器字符串操作源地址变址/通用寄存器DI目标变址寄存器字符串操作目标地址指针寄存器SP堆栈指针始终指向栈顶指针寄存器BP基址指针访问栈帧、局部参数段寄存器CS代码段寄存器指向当前代码段段寄存器DS数据段寄存器指向当前数据段段寄存器SS堆栈段寄存器指向当前堆栈段段寄存器ES附加段寄存器指向附加数据段控制寄存器IP指令指针指向下一条要执行的指令控制寄存器FLAGS标志寄存器存放状态位和控制位这张表建议读者抄一遍再默写一遍。刚开始不用记住每个寄存器的所有细节但至少要能在脑子里画出这张表的分区。2.2 三段式分工算数、地址、状态各管一摊这14个寄存器按照职责可以分成三块理解比死记名单高效得多。第一块是干活的包括AX、BX、CX、DX、SI、DI、SP、BP这8个通用寄存器。它们的特点是大多数指令都能用主要用来存数据、存地址、配合寻址。第二块是搭框架的包括CS、DS、SS、ES这4个段寄存器。它们不参与普通的加减乘除而是负责划定内存中代码在哪里、数据在哪里、栈在哪里、额外数据在哪里这四个区域。之所以需要它们是因为8086的寄存器只有16位却要访问1MB的内存空间后面专门讲这个矛盾怎么解决。第三块是做决策的包括IP和FLAGS。IP保存下一条指令在代码段中的偏移地址CPU每执行一条指令就自动把它改掉FLAGS保存上一次运算产生的一系列状态比如结果是0还是1、有没有进位、是正还是负条件跳转指令全靠它来决定走哪个分支。这样一分类14个名字就不是14个孤立单词而是一个分工明确的团队。2.3 高低字节的拆分AL、AH、BL、BH16位寄存器还有一个容易忽略的细节AX、BX、CX、DX这四个寄存器可以按高8位和低8位单独使用。AX的高8位叫AH低8位叫ALBX对应BH、BLCX对应CH、CLDX对应DH、DL。这种拆分带来两个直接影响。第一写字节型数据时汇编器知道你是按16位操作还是按8位操作mov ax, 1234h和mov al, 34h是完全不同的两件事。第二某些指令天生只认低8位比如用CL存移位次数用AL存乘法的一个操作数如果你只有完整16位的概念一旦看到指令里出现AL、CL就会发懵。我见过不少初学者把AH、AL当成另两个独立寄存器这也不算错但要清楚它们的物理位置就在AX内部改AL不会动AH改AH也不会动AL但整个AX的值会随之变化。把这个关系刻在脑子里后面调试时能少走很多弯路。3. AX、BX、CX、DX这四个家伙的隐藏身份3.1 AX累加器很多指令的默认玩家AX全名Accumulator累加器。说是通用寄存器但它有一堆默认用途。最典型的是乘除法。mul指令的规则是这样的8位乘法AL乘以另一个8位数结果放到AX16位乘法AX乘以另一个16位数结果的高16位放DX、低16位放AX。也就是说无论你愿意不愿意乘法指令默认其中一个操作数必须在AL或AX里结果也只能写到固定的地方。div除法同样如此16位除法要求被除数在AX中32位除法要求被除数在DX:AX组合中。如果一个函数里连续做几次乘除法就会发现AX和DX被反复进出数据这不是代码设计有问题而是硬件规定的默认约定。理解了这一点再看那些mov dx, 0再div的固定套路就不会觉得莫名其妙了——因为32位被除数需要先通过mov dx, 0把高16位清零。3.2 BX基址寄存器唯一能直接当偏移用的通用寄存器BX的特别之处在于它不仅能存普通数值还能直接当作内存偏移地址来用。比如mov ax, [bx]意思是把DS段中偏移地址为BX的那个内存单元里的内容取到AX。直接支持[BX]、[SI]、[DI]这种用寄存器做偏移的寻址方式BX是其中一个主力。这个特性在查表、遍历数组时特别好用。比如有一张16个元素的表格你想依次读取只要让BX从0递增到15每条mov al, [tablebx]都能取到对应元素。在高级语言里这叫数组下标在汇编里BX就扮演了下标的角色。需要注意的是BX虽然叫基址寄存器但它不等于段地址。真正决定哪一段内存的是段寄存器DSBX只提供在这一段里的第几个字节。段地址和偏移地址的配合是这一章的核心。3.3 CX循环计数器loop指令的指定搭档CX全名Count Register计数寄存器最经典的用法就是配loop指令。loop的执行逻辑只有两步先把CX减1再判断CX是不是0不是0就跳回到目标地址是0就继续往后执行。注意是先减后判断所以如果循环开始前CX等于5实际会循环5次如果CX初始就是0第一次loop会先把CX变成FFFFH再判断不为0结果直接进入一个65536次的意外长循环。用CX做循环计数的代码长这样mov ax, 0 mov cx, 5 s: add ax, 2 loop s执行完之后AX等于10CX等于0。很多人在循环体里写了改CX的指令导致循环次数完全失控这是经典的CX使用事故。3.4 DX数据寄存器AX的高16位搭档DX最常出现的场景有两种。第一种是前面说的乘除法mul/div会把32位结果或被除数拆成DX:AX两部分DX保存高16位第二种是I/O操作端口读写指令in/out要用DX指定端口号。做个简单总结AX、CX、DX这三兄弟的身份其实都写在名字里AX管累加、CX管计数、DX管数据的扩展部分。只有BX的基址用途在字面上不那么明显需要多花点时间去体会。记住这些隐藏身份不仅是为了应付考试更重要的是在读别人写的汇编代码时能一眼看穿某条指令为什么非要用AX而不是BX。这种代码意图识别能力在逆向分析和底层优化里特别值钱。4. 物理地址怎么来的段地址×16加偏移地址的完整推演4.1 16位寄存器怎么覆盖1MB内存这是寄存器这一章公认最难、也最值得讲透的环节。经典8086有20根地址线理论上能访问2^20字节也就是1MB内存地址范围从00000H到FFFFFH。但寄存器只有16位最大只能表达65535也就是64KB范围的地址直接拿一个寄存器当内存地址无论如何都够不着1MB。解决思路很巧妙用两个16位的值拼一个20位的值。其中一个叫段地址另一个叫偏移地址组合规则是物理地址 段地址 × 16 偏移地址这个×16不是随便选的因为16等于2的4次方把段地址左移4个二进制位正好能补出一个20位地址。一个段地址有65536种可能一段内的偏移也有65536种可能二者组合出来的逻辑地址空间远大于1MB但物理上能被访问到的还是那1MB——这就为一个物理地址对应多种逻辑写法埋下了伏笔。4.2 手动推演一个物理地址直接看例子。假设当前CS1000HIP2000H那么CPU即将取出的指令的物理地址是多少段地址1000H乘以16等于10000H加上偏移地址2000H得到12000H。具体到二进制看更清楚段地址1000H的二进制是0001 0000 0000 0000左移4位变成0001 0000 0000 0000 0000即10000H偏移地址2000H的低4位的空间正好从0000H到FFFFH可以完整覆盖段内偏移。整个组合过程就是把段地址当页码把偏移地址当页内行号。这个计算一定要亲手算几遍直到不假思索就能口算。之后学调试时你看到CS:IP就能立刻估算出CPU正在执行哪段内存里的代码这是读懂程序执行轨迹的基本功。4.3 同一个物理地址为什么有多种写法既然物理地址由两个16位值拼出来那两个值稍微调整一下完全可能算出同一个物理地址。还是用刚才的12000H举例。它可以用CS1000H、IP2000H来表示也可以用CS1200H、IP0000H来表示1200H乘以16等于12000H加上0000H还是12000H。甚至可以用CS0FF0H、IP3000H来表示算出来同样是12000H。这意味着什么呢意味着段寄存器的值和某个内存单元的地址不是一一对应关系。段寄存器只是提供了一种视角同一个物理地址可以落在不同的逻辑区间里具体怎么安排完全看程序怎么组织。这个特性在操作系统内存管理、多任务切换时尤其重要。你还会看到逻辑地址线性地址物理地址这些说法它们的源头就在这个×16的拼地址公式里。4.4 段寄存器的默认搭档段寄存器一共有4个每个都有固定的默认搭档CS:IP组合决定下一条要执行的指令在哪里。IP是偏移地址。SS:SP组合决定栈顶在哪里。SP提供栈顶偏移SS提供栈所在的段。DS配合[BX]、[SI]、[DI]等偏移访问默认数据段。ES通常配合DI用于字符串操作的目标地址。初学者最容易犯的错是看到mov ax, [bx]以为这是从BX寄存器里取值其实它是从DS:BX对应的内存单元里取值。没有中括号的bx是寄存器本身有中括号的[bx]才是以BX为偏移地址的内存单元。这两者差异是后面第7节第一个坑的主角。5. 标志寄存器一个寄存器里藏着的9张状态纸条5.1 状态标志 vs 控制标志FLAGS是16位寄存器但它不是一个用来存普通数字的寄存器而是把每一位当成一个独立开关。经典8086的有效标志位一共9个分成两类。第一类是6个状态标志记录运算结果的特征CPU自动设置程序员通常不需要手动改CF进位标志无符号数运算时最高位发生进位或借位则置1。PF奇偶标志结果低8位中1的个数为偶数则置1。AF辅助进位标志低4位向高4位有进位或借位时置1BCD码运算会用到。ZF零标志结果为0则置1。SF符号标志结果最高位为1也就是从有符号数角度看是负数则置1。OF溢出标志有符号数运算结果超出范围则置1。第二类是3个控制标志用来控制CPU行为往往需要指令显式设置DF方向标志决定字符串处理指令是正向还是反向。IF中断允许标志决定是否响应可屏蔽中断。TF陷阱标志单步跟踪标志调试器设置它之后CPU每执行一条指令就停下来一次。状态标志不需要主动去赋1或清0它是运算的副产物跟体温计一样烧不烧不由体温计决定。5.2 用一条加法看清CF和OF的区别很多人在CF和OF上栽跟头两者都叫溢出到底有什么区别答案是视角不同CF看无符号数OF看有符号数。拿8位寄存器举例执行mov al, 78h add al, 78h0x78是十进制的120。120加120等于240写成二进制是1111 0000。站在无符号数0~255的视角240没有超出255所以CF0站在有符号数-128~127的视角240对应的补码是-16已经超出了127所以OF1。同时这个结果不是0ZF0最高位是1SF111110000里1的个数是4个偶数PF1低4位8816产生进位AF1。一条add指令六个状态标志全都有反应这种例子最能看出标志位的独立性。如果换一条指令比如mov al, 0F0h再加add al, 10h结果是24016256AL只能存低位0所以AL0CF1ZF1但从有符号角度-16160没有越界OF0。这类例子动手跑一遍比背十遍定义都管用。5.3 条件跳转指令的决策依据CPU为什么要在每次运算后都记录这些标志位因为后面的程序要根据运算结果来决定执行哪条分支。比如cmp ax, bx这条指令本质上就是做一次ax减bx的减法但不保存结果只更新标志位。紧接着的jz某个标签就是如果上一次比较结果是零ZF1就跳转。完全不看标志位就没法理解jz、jnz、jb、ja这些指令的含义反过来掌握了标志位规则读条件分支就像读如果……那么……的自然语言。我建议读者在调试工具里跑几段带cmp和jz的代码单步观察每个标志的变化这样会比只做选择题牢固得多。6. 实战用寄存器驱动三段小程序6.1 环境说明手头有什么就用什么这一章实操不需要高端IDE任何一个支持8086汇编的调试环境都行哪怕是在线模拟器也没问题。重要的是学会单步执行查看寄存器窗口这个动作。我用的环境支持直接写指令、单步运行、右侧有寄存器值变化列表读者换成自己手头的工具即可。下面的代码不涉及中断、不涉及操作系统调用纯粹在寄存器层面做实验所以环境差异不会影响结果。6.2 程序1一条加法验证CF、ZF、OF输入下面三行mov ax, 0FFFFh mov bx, 0001h add ax, bx单步执行到最后一行观察AX和标志位变化。0FFFFh加1等于10000h但AX只有16位只能保存低16位的0000h多出来的最高位进位被记录到CF。你会发现AX0000hCF1ZF1OF0。这是因为从无符号角度看FFFF1溢出到10000CF该置1从有符号角度看-110没有溢出OF保持0。这个例子把进位和溢出彻底分开是我最推荐给初学者跑的第一个实验。6.3 程序2loop循环里CX的递减轨迹再用loop写一个累加mov ax, 0 mov cx, 5 s: add ax, 2 loop s执行时盯着CX看每执行一次loopCX先减1随后如果还不为0就跳回s标签处。CX从5依次变成4、3、2、1、0跳转了5次AX从0累加到10。这里最值得观察的细节是loop的先减后判当CX已经是1时再执行一次loop会让它变成0并决定不再跳转。如果循环开始前CX就设成0后果是第一次loop把CX变成FFFFH循环65536次这是所有教科书都会强调的经典坑。6.4 程序3通过DS访问内存抓好段偏移的感觉这个例子稍微提前引入一点内存访问目的是把第4节的公式用起来mov ax, 1000h mov ds, ax mov ax, 1234h mov [0], ax mov bx, [0]第一行把1000h放进AX第二行把AX传给DS第三行把1234h放进AX第四行把AX的值写到DS:0000h这个内存单元也就是物理地址10000h处第五行再从DS:0000h读回来放进BX。最终BX等于1234h同时你可以在内存窗口里看到10000h处被写入了34h、12h两个字节这正好体现了经典8086的小端存放方式。这个实验做完你对段地址决定一段区域偏移地址决定区域内哪一个字节这句话会有直观感受。6.5 一套可复用的读寄存器检查清单实战课最容易陷入代码跑通就完事的状态。我习惯在每次单步后用这套检查清单过一遍这个寄存器为什么变是立即数赋值、还是运算结果、还是从内存读出来标志位里CF和OF有没有变化如果运算涉及加减一定要解释清进位不是溢出。如果用了loopCX的值和跳转次数是否吻合有没有被循环体内代码意外修改如果涉及内存段寄存器当前指向哪里偏移地址和物理地址分别是什么把这四个问题问一遍比多抄十遍代码都管用。7. 初学寄存器时最容易踩的四个坑7.1 坑1中括号的有无天差地别mov ax, bx和mov ax, [bx]这两行新手经常混着写。前者是把BX寄存器里的数值复制给AX比如BX里是1234hAX就变成1234h后者是把内存中DS:BX指向的那个16位数据取出来给AX。有没有中括号决定了操作数是寄存器本身还是寄存器指向的内存单元。判断窍门只有一条看到方括号就当按地址去取值。同样mov ax, 3是立即数3mov ax, [3]是内存地址0003h处的内容两者相差甚远。写代码时先问自己一句我现在想取的是数值还是这个数值放在哪里的内容7.2 坑2段寄存器不能直接塞立即数有人想给DS赋值直接写mov ds, 1000h汇编器立刻报错。经典8086指令集里没有段寄存器←立即数这种编码形式必须通过通用寄存器中转mov ax, 1000h mov ds, ax这是很多人第一次写DS相关代码就遇到的拦路虎。原因不是设计缺陷而是指令编码空间有限段寄存器的赋值被设计成只支持寄存器到段寄存器。这也解释了为什么几乎所有汇编程序里都能看到先mov通用寄存器再mov段寄存器的固定两连击。7.3 坑3循环体里悄悄改掉CXCX是loop的循环计数器可它同时也是通用寄存器。很多初学者在循环体里图方便用CX临时存别的数据发现循环次数彻底失控可能只循环一次就退出也可能进入几万次的长循环。这种bug在单步调试时最迷惑因为表面上CX的值看起来没毛病。解决办法是养成习惯循环计数用CX循环体里如果需要暂存数据优先用别的寄存器实在避不开就在进入循环前把影响CX的操作全都规划好。真正工程化的做法是涉及子程序时用栈保存现场后续学到栈操作后可以回来看这个坑的工程解法。7.4 坑4只盯结果值不看标志位add ax, bx执行完AX里确实是运算结果但如果你只关注AX就可能丢掉CF、OF、ZF、SF这四个关键信息。尤其当后续代码要用adc带进位加法或条件跳转时错误就会从几行之后爆出来。举个典型场景做多字节加法时先add低16位再adc高16位。adc的意思是加上一个操作数再加上CF如果你没有先确认低16位加法是否产生进位高16位就会完全算错。这类问题在单步执行时一眼就能看出来CF是否在低16位加法后变成了1决定了adc要不要加那个1。看到这里其实你离把寄存器真正用起来已经不远了。我自己学这一章时的体会是千万不要把这14个名字当成静态表格来背一定要在调试器里反复单步亲手观察它们的变化。现在觉得麻烦后面学寻址方式、栈、中断时就会顺滑很多。
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →