兰陵月 发表于 2017-11-8 13:55:05

X86汇编语言-从实模式到保护模式—笔记(14)【多图,手鸡慎入】

本帖最后由 兰陵月 于 2017-12-5 21:54 编辑

接——X86汇编语言-从实模式到保护模式—学习笔记(十三)


从前面的知识我们可以知道,Intel处理器允许256个中断,中断号的范围是0~255,中断代理8259芯片负责提供其中的15个,但中断号并不固定。之所以不固定,是因为当初设计的时候,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in和out指令来改变它的状态,包括各引脚的中断号。因此,它又叫可编程中断控制器(PIC)。
如上图,在个人计算机上使用中断代理控制器8259芯片,需要两块。第一块8259芯片的代理输出INT直接送到处理的INTR引脚,这是主片(Master);第二块8259芯片的INT输出送到第一块的引脚2上(IR2),是从片(Slave)。两块芯片之间形成级联(Cascade)关系。8259的主片引脚0(IR0)接的是系统定时器/计数器芯片;从片的引脚0(IR0)接的是实时时钟芯片RTC。主片IR0优先级最高,IR7优先级最低,从片也是如此,当然还要考虑到级联关系。
在8259芯片内部,有中断屏蔽寄存器(IMR),这是个8位寄存器,对应着该芯片的8个中断输入引脚,对应的位是0还是1,决定了从该引脚来的中断信号是否能够通过8259送往处理器(0表示允许,1表示阻断)。当外部设备通过某个引脚送来一个中断请求信号时,如果它没有被IMR阻断,那么,它可以被送往处理器。
8259芯片是可编程的,主片的端口号是0x20和0x21,从片的端口号是0xa0和0xa1,可以通过这些端口访问8259芯片,设置它的工作方式,包括IMR的内容。当然,中断能否被处理,除了要看8259芯片的脸色外,最终的决定权在处理器手中。在处理器内部,标志寄存器有一个标志位IF,这就是中断标志。当IF为0时,所有从处理器INTR引脚来的中断信号都被忽略掉;当其为1时,处理器可以接受和响应中断。指令cli和sti可以改变IF标志位的值,cli用于清除IF标志位,sti用于置位IF标志位。
实时时钟电路(RTC)和静态存储器(CMOS RAM)都集中在外围设备控制芯片ICH内部。实时时钟是全天候跳动的,因为它由主板上的一个小电池提供能量。除了日期和时间的保存功能外,RTC芯片也可以提供闹钟和周期性的中断功能。该芯片由一个振荡频率为32.768kHz的石英晶体振荡器(晶振)驱动,经分频后,用于对CMOS RAM进行每秒一次的时间刷新。
日期和时间信息保存在CMOS RAM中的,通常有128字节,而日期和时间信息只占了一小部分容量,其他空间则用于保存整机的配置信息,比如各种硬件的类型和工作参数、开机密码和辅助存储设备的启动顺序等。这些参数的修改通常在BIOS SETUP开机程序中进行。如下图所示:

如上图,常规的日期和时间信息占据了CMOS RAM开始部分的10字节,有年、月、日和时、分、秒,报警的时、分、秒用于产生到时间报警中断,如果它们的内容为0xC0~0xFF,则表示不使用报警功能。
CMOS RAM的访问,需要通过两个端口来进行。0x70或者0x74是索引端口,用来指定CMOS RAM内的单元;0x71或者0x75是数据端口,用来读写相应单元里的内容。
端口0x70的最高位(bit 7)是控制NMI中断的开关。当它为0时,允许NMI中断到达处理器,为1时,则阻断所有的NMI信号,其他7个比特,即0~6位,则实际上用于指定CMOS RAM单元的索引号。
RTC芯片的中断信号,通向中断控制器8259从片的第1个中断引脚IR0。在计算机启动期间,BIOS会初始化中断控制器,将主片的中断号设为从0x08开始,将从片的中断号设为从0x70开始。所以,计算机启动后,RTC芯片的中断号默认是0x70。我们可以通过编程改变它,但是在这里并没有必要。


本章的例子是实时时钟的中断例程。要让中断处理程序按照我们的意思来做,当然就要编写的我们自己的程序,并且把实时时钟的中断处理程序的入口点写入中断向量表(IVT)里相应的位置处,什么位置呢?当然实时时钟中断号在中断向量表里对应的位置。具体计算就是段地址=中断号×4+2,偏低地址=中断号×4。
程序继续执行到第132行,如下图:

值,在IVT中以该值为偏移地址的地方存放着该中断的入口点偏移地址。该值+2处存放着就是该中断入口点段地址。
第132行、第133行,将中断号“0x70”给寄存器al,bl为乘数。
第134行,得到积,存放在寄存器AX中。第135行,将存放中断程序入口点偏移地址的IVT偏移地址给寄存器BX。准备进行下一步处理。


第137行,在修改中断向量表时,需要先用cli指令清中断,即将标志寄存器IF清零。否则当表项信息只修改了一部分的时候,如果发生了0x70号中断,则会产生不可预料的问题。


第139行~第145行,修改IVT表项中相应位置的内容,修改完以后,如果发生了0x70号中断,则会在IVT表项中寻找中断处理程序入口点的段地址和偏移地址,用其修改CS和IP的值,再跳转到相应的位置处执行中断处理。
第139行、第145行,因为中间要用到寄存器ES,因此第139行将ES的值压栈,第145行,使用ES结束后再将ES值弹出恢复。
第140行、第141行,IVT是从段地址0x0000开始的,这两句将段地址0x0000给寄存器ES。使ES指向中断向量表所在的段。
第142行,将标号“new_int_0x70”的值作为入口点偏移地址,放入ES段偏移地址为BX的地方。第144行,将段地址放入其后偏移为“BX+2”的地方。
处理器执行完上述指令后,就将中断处理程序的入口地址在IVT表中作了登记。以后当处理器响应0x70中断时,就直接从IVT表中相应偏移地址处取出入口地址,转到中断处理程序执行。

接下来,我们要设置RTC的工作状态,使它能够产生中断信号给8259中断控制器。
RTC到8259的中断线只有一根,而RTC可以产生多种中断。比如闹钟中断、更新结束中断和周期性中断。RTC的计时(更新周期)是独立的,产生中断信号只是它的一个赠品。所以,如果希望它能产生中断信号,需要额外设置。最简单的就是设置更新周期结束中断。每当RTC更新了CMOS RAM中的日期和时间后,将发出此中断。更新周期每秒进行一次,因此该中断也每秒发生一次。

为了设置“更新周期结束中断”,需要对RTC的寄存器B进行写操作,并且在访问RTC期间,最好是阻断NMI。根据前面“端口0x70的最高位(bit 7)是控制NMI中断的开关。当它为0时,允许NMI中断到达处理器,为1时,则阻断所有的NMI信号,其他7个比特,即0~6位,则实际上用于指定CMOS RAM单元的索引号。”的知识,第147行,将RTC寄存器B的索引值给寄存器AL,并在第148行,通过“or al,0x80”(0x80=1000 0000B)将AL的最高位置1,表示阻断所有的NMI信号。第149行,将AL的值写入端口号0x70,指定要访问的CMOS RAM内的单元—RTC寄存器B。
第150行,设定要写入寄存器B的值,第7位:允许更新周期照常发生,值0;第6位,周期性中断允许禁止,值0;第5位闹钟中选允许禁止,值0;第4位,更新结束中断允许,值1;第3位,方波允许位,该位空置,值0;第2位,数据模式:BCD,值0;第1位,小时格式,24小时,值1;第0位,老软件的夏令时支持,该功能不再支持且无用,值0。因此要写入寄存器B的值从第7位到第0位分别是:0、0、0、1、0、0、1、0,即00010010B,即0x12。因此将“0x12”给AL。
第151行,向端口“0x71”写入“0x12”。至此,寄存器B设置完毕。


第153行,将寄存器C的索引值给AL。第154行,向端口“0x70”写入寄存器C的索引值。0x0C=0000 1100B,其最高位为“0”,根据知识点“端口0x70的最高位(bit 7)是控制NMI中断的开关。当它为0时,允许NMI中断到达处理器,为1时,则阻断所有的NMI信号,其他7个比特,即0~6位,则实际上用于指定CMOS RAM单元的索引号”,在向索引端口“0x70”写入“0x0C”的同时,控制NMI中断的开关被置0,允许NMI中断到达处理器。第155行,从寄存器C读一个字节,也就是随便读一下,之后将开始产生中断信号。


根据知识“8259芯片是可编程的,主片的端口号是0x20和0x21,从片的端口号是0xa0和0xa1,可以通过这些端口访问8259芯片,设置它的工作方式,包括IMR的内容。”,我们从8259从片端口“0xa1”读出中断屏蔽寄存器IMR的值。在第158行,用“and”指令将第0位置“0”,“ 0xfe=1111 1110B”。第159行,再将这个值写回8259从片的IMR寄存器,这样RTC的中断可以被8259处理了。


至此,RTC和8259的工作状态、参数都已经设置完毕。第161行,“sti”指令置位标志位IF,开放设备中断。从这个时候开始,中断随时都会发生,也随时会被处理。


显示两个提示信息,工作过程同上,不再重复。


在屏幕上第12行第35列,显示字符“@”。


第174行,指令“hlt”使处理器停止执行指令,并处于停机状态,这将降低处理器的功耗。处于停机状态的处理器可以被外部中断唤醒并恢复执行,而且会继续执行hlt后面的指令。
第175行,not指令将字符“@”的显示属性反转。not是按位取反指令,其格式为not r/m8 ,not r/m16。not指令执行时,会将操作数的每一位反转,原来的0变成1,原来的1变成0。not指令不影响任何标志位。
第173到第176行,形成一个循环,先是停机,接着某个外部中断使处理器恢复执行。一旦处理器的执行点来到hlt指令之后,则立即使它继续处于停机状态。

以上就是用户程序的主程序,停机、执行,接着停机。与此同时,中断也在不断发生着,期间处理器还要抽空执行中断处理程序。

中断处理程序从第27行开始执行,如下图:

第28行~第32行,先保护好现场,将后面用到的寄存器压栈保存。这一点特别重要,中断处理过程必须无痕地执行,你不知道中断会在什么时候发生,也不知道中断发生时,哪一个程序正在执行,所以,必须保证中断返回时,能还原中断前的状态。

指令test是测试的意思,可以用这条指令来测试某个寄存器,或者内存单元里的内容是否带有某个特征。test指令在功能上和and指令是一样的,都是将两个操作数按位进行逻辑“与”,并根据结果设置相应的标志位。但是,test指令执行后,运算结果被丢弃(不改变或破坏两个操作数的内容)。test指令需要两个操作数,其指令格式为:
test r/m8,imm8
test r/m16,imm16
test r/m8,r8
test r/m16,r16
test指令和and指令一样,执行后,0F=CF=0;对ZF、SF和PF的影响视测试结果而定;对AF的影响未定义。比如,我们想测试寄存器AL的第3位是“0”还是“1”,可以这样编写代码:test al,0x08。0x08的二进制形式为00001000,它的第3位是“1”,表明我们关注的是这一位。不管寄存器AL中的内容是什么,只要它的第3位是“0”,这条指令执行后,结果一定是00000000,标志位ZF=1;相反,如果寄存器AL的第3位是“1”,那么结果一定是00001000,ZF=0。于是,根据ZF标志位的情况,就可以判定寄存器AL中的第3位是“0”还是“1”。


第34行~第40行,从数据端口读取CMOS RAM寄存器A的值,根据CMOS RAM寄存器A中UIP位的状态来决定是等待更新周期结束,还是继续往下执行。UIP位为0表示现在访问CMOS RAM中的日期和时间是安全的。第36行,用于把寄存器AL的最高位置1,从而阻断NMI。当然,这是不必要的,当NMI发生时,整个计算机都应当停止工作,也不在乎中断处理过程能否正常执行。第37行,将“10001010B”写入端口“0x70”,指定后面要访问的CMOS RAM寄存器A。第38行,从端口“0x71”取CMOS RAM寄存器A的数据。第39行,测试寄存器AL的第7位是否为1。第40行,如果寄存器AL中第7位为“0”,则说明CMOS RAM寄存器A中UIP位值为“0”,说明更新周期至少在488微秒内不会启动(此时访问CMOS RAM中的时间、日历和闹钟信息是安全的),。“jnz .w0”跳转条件不成立,程序往第42行执行。如果寄存器AL中第7位为“1”,则说明CMOS RAM寄存器A中UIP位值为“1”,说明“正处于更新周期,或者马上就要启动”(此时访问CMOS RAMA中的信息是不安全的,不应该访问),“jnz .w0”跳转条件成立,程序跳转返回至第34行继续等待RTC更新周期结束。

本例程特别说明:正常情况下,访问CMOS RAM中的日期和时间,必须等待RTC更新周期结束,所以上面的判断过程时必需的,而这些代码也适用于正常的访问过程。但是,当前中断处理过程时针对更新周期结束中断的,而当此中断发生时,本身就说明对CMOS RAM的访问是安全的,毕竟留给我们的时间是999毫秒,这段时间非常充裕,处理器能够执行千万条指令。所以,在这种特定的情况下,上面的判断过程是不必要的。当然,加上倒也无所谓。


第42行到第46行,读取CMOS RAM中的0号单元-“秒”的数据,压栈进行保存。第42行,将寄存器AL清零。第43行,将寄存器AL的最高位置1。第44行,向端口“0x70”写入寄存器AL的值,表示要访问第0号单元。第45行,从端口“0x71”取第0号单元—“秒”的数据,放入寄存器AL中。
第48行到第52行,读取CMOS RAM中的2号单元-“分”的数据,压栈进行保存。第54行到第58行,读取CMOS RAM中的4号单元-“时”的数据,压栈进行保存。


第60行~第62行,读一下CMOS RAM中寄存器C,使得所有中断标志位复位(仅指更新周期结束中断,不包括周期性中断和闹钟中断等)。这等于是告诉RTC,中断已经得到处理,可以继续下一次中断。否则的话,RTC看到中断未被处理,将不再产生中断信号。RTC产生中断的原因有多种,可以在程序中通过读寄存器C来判断具体的原因。不过这里不需要,因为除了更新周期结束中断外,其他中断都被关闭了。


第64行、第65行,临时将段寄存器ES指向显示缓冲区。


第67行,通过前面的运行我们可以知道,最后压入栈中的是CMOS RAM中“时”的数据,从栈中弹出“时”的数据给寄存器AX,作为参数。第68行,调用过程“bcd_to_ascii”来将用BCD表示的“小时”转换成ASCII。

过程“bcd_to_ascii”在程序第105行,如下图:

AX中低8位存有“小时”数据。其中“小时”十位在AL中的高4位,个位在AL中的低4位。第108行,将AL的值给AH。第109行,“0x0f”的二进制为“0000 1111B”,执行完“and al,0x0f”后,al中的低四位保留,高4位清零,这样就得到了“小时”数据的个位。第110行,将个位转化为ASCII码的值。
第112行,将寄存器AH的值逻辑右移4位,AH的高4位移到了低4位,而其高4位本身就是“小时”数据的十位。第113行,同样只保留低4位的值(此时低4位经逻辑右移过来之后,本身值为“小时”数据的十位)。第114行,将其转化为ASCII码值。
第116行,过程“bcd_to_ascii”执行完毕之后,程序返回至第69行执行,“小时”数据转化成的ASCII码值在寄存器AX中,AX作为过程调用的结果返回给调用者,其中AH中放置十位,AL中放置个位。

程序返回至第69行执行,如下图:

第69行设置,为了连续在屏幕上显示内容,最好是采用基址寻址来访问显存。这一行用于指定显示的内容位于显存的什么位置。实际上,这里指定的是第12行36列。同以前一样,每个字符在显存中占两个字节,每行80个字符,所以这里使用了表达式12*160+36*2,该表达式的值是在编译阶段计算的。


第71行、第72行,分别将“小时”的两个数位写到显存中,段地址在ES中,偏移地址分别是由寄存器BX和BX+2提供。这里没有写入显示属性,这是因为我们希望采用默认的显示属性(屏幕是黑的,默认的显示属性是0x07,黑底白字)。


第74行、第75行用于在屏幕下一个位置显示一个“:”,这两句可以合成一句,即“mov byte ,”:””这里笔者重复了一下,并在书中作了解释。第76行,指令“not”反转这个“:”的显示属性。


程序第78行~第90行,同上面的过程一样,将得到的分钟和秒的数值转换成ASCII码,然后紧着显示在屏幕上。当然秒的数值显示结束后,就不要在显示“:”了。

在8259芯片内部,有一个中断服务寄存器(ISR),这是一个8位寄存器,每一位都对应着一个中断输入引脚。当当中断处理过程开始时,8259芯片会将相应的位置1,表明正在服务从该引脚来的中断。
一旦响应了中断,8259中断控制器无法知道该中断什么时候才能处理结束。同时,如果不清除相应的位,下次从同一个引脚出现的中断将得不到处理。在这种情况下,需要程序在中断处理过程的结尾,显式地对8259芯片变成来清除该标志位,方法是向8259芯片发送中断结束命令(EOI)。中断结束命令的代码是0x20。需要注意的是如果外部中断是8259主片处理的,那么,EOI命令仅发送给主片即可,端口号是0x20;如果外部中断是由从片处理的恶,就像本章的例子,那么,EOI命令既要发往从片(端口0xa0),也要发往主片。


如上图,发送中断结束命令EOI。第93行,向从片发送EOI。第94行,向主片发送EOI。


中断程序执行完毕,第96行~第100行,恢复现场。第102行,用中断返回指令“iret”返回到中断之前的地方继续执行。
整个程序到此结束。

程序运行情况如下:

可以在Boch中运行,但是秒数跳动特别快,估计是软件的原因。


可以在VMware中正确运行,并且也未发生像Boch模拟器那样的情况。
页: [1]
查看完整版本: X86汇编语言-从实模式到保护模式—笔记(14)【多图,手鸡慎入】