X86汇编语言-从实模式到保护模式—笔记(13)【多图,手鸡慎入】
本帖最后由 兰陵月 于 2017-12-5 21:54 编辑接——X86汇编语言-从实模式到保护模式—学习笔记(九)
主引导扇区程序将用户程序全部加载之后,就开始处理用户程序的头部,主要处理的内容有:1、重新处理用户程序入口点所在段的段地址并回填,重新处理其他段的段地址并回填,头部所在段的段地址不需要作处理,因为这是处理器的事情,且用户程序需要实现的一些也不需要修改头部所在段的段地址。
处理完用户程序的头部之后,主引导扇区会用一个“jmp far [偏移地址]”指令,跳转到用户程序入口点处开始执行。
jmp far ;第50行(位于主引导扇区程序内)
【从主引导扇区程序第50行跳转而来】
第119行为用户程序入口点。从主引导扇区程序第50行跳转而来
第120、121、122行,将用户程序所使用的栈的段地址给了段寄存器SS,并确定了栈指针SP。第122行运行完毕之后,段寄存器SS栈指针指向下图中第297行所处的段,栈指针SP指向指向第300行处的标号处,标号“ss_pointer”的值为256,即栈指针SP的值为256D。“mov sp,ss_pointer”实际上等于“mov sp,256”。
我们可以看到这个栈的大小是256字节,即128个字。
上图中伪指令“resb”的意思是从当前位置开始,保留指定数量的字节,但不初始化它们的值,在源程序编译时,编译器会保留一段内存区域,用来存放编译后的内容。当它看到这条伪指令时,它仅仅是跳过指定数量的字节,而不管里面的原始内容是什么。内存是反复使用的,谁也无法知道以前的使用者在这里留下了什么。也就是说,跳过的这段空间,每个字节的值是不确定的。
【知识点】栈无疑是很重要的,不能被破坏。绝大多数时候,对栈的改变是分两步进行的,先改变段寄存器SS的内容,接着又修改栈指针寄存器SP的内容。假如在刚刚修改了段寄存器SS—比如第121行处执行完修改了段寄存器SS,在还没来得及修改SP的情况下,就发生了中断,会出现什么后果呢?因为中断需要使用栈,而栈不是单独使用SS就可以了,需要SP与其配合才能正常读取数据,此时SS已经修改,而SP还没来得及修改时发生中断,而处理器如果在这个时候响应中断,则会将错误的返回地址压入当前SS:SP位置,且不论这个SS:SP是否有效,但处理器处理完中断无法正常返回那就是肯定的了,程序就这样发生了致命错误。
因此,在程序第121、122行执行期间,处理器禁止中断。
第123、124行,将用户程序的数据段data的段地址给了寄存器ds。第124行执行完毕之后,寄存器ds指向下图第286行处所处的段,即data段。
上图中,我们可以看到,data段定义的时候,有子句“vstart=0”,因此,我们可以知道,其段内的标号“init_msg”、“inst_msg”、“done_msg”、“tips_msg”的地址是从第286行开始计算的,并且是从“0”开始计算的。所以,程序第123、124行运行完毕之后,ds:指向标号“init_msg”处,ds:指向标号“inst_msg”处,ds:指向标号“done_msg”处,ds:指向标号“tips_msg”处。
经过上述操作后,段寄存器DS、SS被重新赋值,指向用户程序自身相关段,这样用户程序就可以访问自己的专属数据了。段寄存器CS就不用初始化了,那毕竟是加载器负责的事情,要不然用户程序怎么可能自己执行呢。
要响应中断,处理器就必须能找到处理中断的程序,也就是必须有中断处理程序的入口点地址。而中断程序的入口地点放置在中断向量表中,因此,下一步我们要在中断向量表中安装实时时钟中断的入口点。
为了增加程序的友好性,在安装中断向量之前,我们应该显示一些提示信息。
程序第126行,将标号“init_msg”的值给寄存器bx,作为显示初始信息的参数。
程序第127行,跳转至标号“put_string”处执行。程序第129、130行功能类似,只是显示的信息不同而已。
子程序“put_string”在源程序第179行~第189行。如下图:
程序第181行,将内存单元DS:处的字节传递给寄存器CL。
程序第182行,将CL与自己进行“或”操作。一个数和自己做“或”运算,结果还是它自己,但计算结果会影响标志寄存器中的某些位。如果ZF位置位0,则说明取到了串结束标志“0”,程序转移到第188行处执行,退出过程“put_string”。否则,将取到的字符串作为参数,调用过程“put_char”。过程“put_char”起始处在源程序第192行,结束处在源程序第283行。
字符串未结束的情况下,程序由第183行顺序执行到第184行。
第184行,调用过程“put_char”,跳转到第192行。如下图:
第194行~第199行,由于在子程序“put_char”运行过程中,会破坏调用前的各个寄存器的值,因此,在这里,将各个寄存器的值压栈保护,程序返回前再出栈恢复。
光标是在屏幕上有规律地闪动的一条小横线,通常用于指示下一个要显示的字符位置。因此我们要在屏幕上显示字符,首先就要找到当前光标的位置。
光标在屏幕上的位置保存在显卡内部的两个光标寄存器中,每个寄存器是8位的,合起来形成一个16位的数值。比如,0表示光标在屏幕上第0行第0列,80表示它在第1行第0列,因为标准VGA文本模式是25行,每行80个字符。这样算来,当光标在屏幕右下角时,该值为25×80-1=1999。
光标寄存器是可读可写的,而且显卡从来不自动移动光标位置,移动光标位置实际就是对光标寄存器进行读写操作。显卡内部的寄存器操作只能通过索引寄存器间接访问。索引寄存器的端口号是0x3d4,可以向它写入一个值,用来指定内部的某个寄存器。指定了某个寄存器后,可以通过数据端口0x3d5进行读写。前面提到的光标的两个8位寄存器,其索引值分别是0x0e和0x0f,分别用于提供光标位置的高8位和低8位。
第202行,将0x3d4给寄存器dx,表明等下对0x3d4端口操作。
第203行,将提供光标位置高8位的光标寄存器的索引值0x0e给al。
第204行,将索引值0x0e写入0x3d4端口。表明下面将对提供光标位置高8位的光标寄存器进行操作。
第205行,将0x3d5给寄存器dx,表明即将对0x3d5端口进行操作。
第206行,从0x3d5端口取出一个字节,这个字节就是光标位置高8位数值。
第207行,将光标位置高8位数值给寄存器ah,腾出al来,因为等下还要用al进行操作。
第209行~第213行,用同样的方法取出光标位置低8位,只不过中间的索引值由0x0e变成了0x0f。
程序执行完第213行后,寄存器AH中放置了光标位置的高8位,寄存器AL中放置了光标位置的低8位。因此寄存器AX就是光标位置的16位完整数据。
第214行,将光标位置数据给BX,因为下面的操作中要用到寄存器AX。
执行到第216行,出现了一个比较语句。将cl的值和0x0d相比较。Cl的值来自第82行,也就是从字符串中取来的。0x0d在字符串中代表是回车的意思。这一行的意思是:将取到的字符同回车符进行比较。
第217行,判断第216行的比较结果,如果是回车符,则不符合本行“jnz .put_0a”的跳转条件,不进行跳转,而顺序往下执行第218行。如果不是回车符,则跳转到“.put_0a”处执行。
假如是回车符,那么取到的CL的值肯定为0x0d,那第216行执行后,ZF会被置1。第217行执行时跳转条件不成立,顺序执行第218行。
第218行,将bx的值给ax。bx的值我们从第214行可以得知,是16位的光标位置数据,这里将光标位置数据给了ax。因为等下要用寄存器AX作为被除数。
第219行,将80给寄存器BL,准备进行8位整除除法了。
第220行,被除数为AX,除数为BL,进行8位整除除法。经过整除后,AL中为商,AH中为余数。我们再次重温下下图中的光标位置知识:
下面的理解中,关于光标位置我们这样来表述:光标位于第N行第M列。其中0 =< N =< 24,0 =< M =< 79。
我们可以想象光标位置的两种情况:
第一种:光标位于某一行的开头,即该行第0列;
第二种:光标位于某一行的其他位置。
在第一种情况下,光标位置数据肯定可以被80整除。因为根据上图中的知识,我们可以知道,假如光标位于第0行第0列,则光标数值为0;假如光标位于第1行第0列,则光标数值为80;假如光标位于第2行第0列,则光标数值为160;依次类推,假如光标位于第24行第0列,则光标数值为24×80=1920。不管处于哪一行的第0列,光标数值都可以被80整除。
在第二种情况下,依据上段中的讲述,光标位置数据肯定不可以被80整除。这样就会产生余数。
回车的意思是将光标移动到本行第0列。结合“回车符”的含义,只要将得到的商乘以80,即可以将光标位置数据变为能够被80整除,也即移动到该行第0列处。
程序第221行,将商(在AL中)乘以BL(值为80),就是光标在当前行第0列(即行首)的位置数据。
这里我们只是得到了下一步光标应该处的位置数据,但还没有把光标移过去。同时,也因为回车符只是在本行内操作,并不会有滚屏的需求,因此我们可以直接进行光标位置设置,不需要考虑滚屏的情况,因此程序第222行,这里直接跳转到设置光标子程序“.set_cursor”,进行光标设置。如下图,这是过程“.set_cursos”子程序入口。
第263行~第265行,向0x3d4端口写入准备操作的光标寄存器索引值0x0e。
第266行~第268行,通过0x3d5端口向提供光标位置高8位数据(即索引值为0x0e的光标寄存器)的寄存器写入光标位置数据的高8位,我们已经在第222行得到了光标位置数据,因此光标位置高8位数据在寄存器bh中。
第269行~第274行,用同样的方法将光标位置低8位数据写入索引值为0x0f的光标寄存器中。
经过设置,光标位置就到该行的行首了。那么到这里,这个字符就处理完毕了。
第276行~第281行,将第194行~第199行压入的寄存器值依次恢复。
第283行,经过第276行~第281行的恢复操作后,栈顶又指向了第184行调用过程时压入的IP值,ret指令将此值给当前IP,程序跳转到第185行继续执行。如下图:
第185行,将寄存器BX的值自加1,指向下一个字符。
第186行,无条件跳转到第179行标号“put_string”处,继续开始下一个字符的处理,直到遇到字符“0”(代表本次要显示的字符串已经全部处理完毕),则跳转到第188行标号“.exit”处,这里只有一个指令ret,执行ret指令,返回到调用过程“put_string”处的下一条指令即第166行继续执行。
第166行代码处的执行我们稍后学习,现在我们回到第182行处。
假如不是回车符,那么说明取到的CL的值肯定不是为0x0d,那第216行执行后,ZF会被置0。第217行执行时跳转条件成立,跳转到第225行过程“.put_0a”的入口处。如下图:
第226行,在取到的字符不是0x0d的情况下,再比较这个字符是不是0x0a(换行符)。这里肯定又有两种情况,一种情况是取到的字符是换行符;另一种情况是取到的字符不是换行符。
假如不是回车符,而是换行符的情况,那么第226行比较结果,ZF会被置1。则第227行“jnz .put_other”跳转条件不成立,程序顺序往下执行,即执行第228行。
第228行,寄存器BX中存放的光标位置数据,将其加80,即换到下一行的同一列。
第229行,跳转到标号“.roll_screen”处,即第241行。如下图:
第242行,我们知道如果光标在屏幕最右下角的位置,则光标位置数据为1999。根据前面程序执行,光标位置数据目前存放在寄存器BX中,因此这里将BX与2000进行比较。
第243行,如果BX值小于2000,则说明经过换行后,光标还在当前可显示的屏幕内,不用滚屏,直接将光标设置过去即可。因此,这里当BX小于2000时,“jl .set_cursor”跳转条件成立,程序跳转到标号“.set_cursor”处执行,那处已经学习过,这里不再重复。
第243行,如果BX值大于等于2000,则说明经过换行后,光标已经不在当前可显示的屏幕内,必须要进行滚屏操作。因此,这里当BX大于等于2000时,“jl .set_cursor”跳转条件不成立,程序顺序执行到第245行。
滚屏,实际上就是将屏幕上第2行(从内存数据角度来说是第1行)~第25行(从内存数据角度来说是第24行)的内容整体往上提一行,最后用黑底白字的空白字符填充第25行(从内存数据角度来说是第24行),使这一行什么也不显示。
movsw指令:用于把数据从内存中的一个地方批量地传送(复制)到另一个地方,处理器把它们看成是数据串。movsb的传送以字节为单位,movsw的传送以字为单位。指令执行时,原始数据串的段地址由DS指定,偏移地址由SI指定,简写为DS:SI;要传送到的目的地址由ES:DI指定;传送的字节数(movsb)或者字数(movsw)由CX指定。除此之外,还要指定是正向传送还是反向传送,正向传送是指传送操作的方向是从内存区域的低地址端到高地址端;反向传送则正好相反。正向传送时,每传送一次字节(movsb)或者一个字(movsw),SI和DI加1后者加2;反向传送时,每传送一个字节(movsb)或者一个字(movsw)时,SI和DI减去1或者减去2.不管是正向传送还是反向传送,也不管每次传送的是字节还是字,每传送一次,CX内容自动减一。
CLD指令将方向标志DF清零,以指示传送是正方向的。STD指令将方向标志DF置位1,以指示传送是反方向的。
REP指令:前缀指令REP,意思是CX不为零则重复。rep movsw的操作码是0xF3 0xA5,它将重复执行其后的语句直到CX的内容为零。
第245行~第247行,将0xb800给段寄存器ES,使段寄存器指向显存所在段。
第248行,CLD指令将方向标志DF清零,以指示传送是正方向的。
第249行~第252行,将屏幕上第2行到第25的数据拷贝到屏幕上第1行到第24行。第249行,十六进制数“0xA0”等于十进制数“160”,这个内存中的偏移地址代表屏幕上第2行行首位置,此行将其设定为拟传送数据的源地址。第250行,将DI设置为0,代表将数据拷贝到的地方的起始地址0。第251行,将循环次数1920给寄存器CX。1920这个数字的来由:要拷贝的数据总共24行,每行80个字符,每个字符在显存中占两个字节。因此要拷贝的总字节数就是3840,movsw每次传送一个字,因此总共需要循环的次数就是1920次。
第252行,执行传送操作,重复1920次。
第253行~第258行,将屏幕最下面一行,即第25行(内存数据中为第24行)变成黑底白字的空白字符。
第253行,将3840给寄存器BX作为偏移地址(此时BX内容不是作为光标位置的数据,而是作为字符在实际内存中的偏移地址),该偏移相对于屏幕来说位于最下面一行的行首。
第254行,一行有80个字符,每个字符占用两个字节,因为有160个字节的内容需要改写,每次改写2个字节,需要循环次数80次。将循环次数80给寄存器CX。
第256行,一个黑底白字的空白字符在两个连续的字节中表示方法是:前面的字节显示空白字符的ASCII码:0010 0000,后面一个字节显示颜色属性黑底白字:0000 0111,因此其在内存中的排列如下面的示意图:
如上图所示,我们只要重复80次,把“0x0720”按字取代屏幕第25行(内存中第24行)在内存中的内容,就能使该行变成黑底白字的空白字符。第253行~第258行正是实现了这样的功能。
第260行,此处将1920作为参数给了寄存器BX。此时的1920并不是偏移地址,而是光标位置的数据,程序从第258行执行过来后,光标的位置应该是位屏幕上第25行的行首,因此将1920给了BX。第253行和此行BX中的数据意义一定要区分开来。
继续往下执行,将光标设置到屏幕上第25行的行首。再继续执行,又返回到第184行,继续执行,然后继续第179行到第186行的循环,取下一个字符,进行判断,直到要显示的字符全部显示完毕。
假如不是回车符,也不是换行符的情况,而是其他字符的情况:这种情况下,处理器取到了真正要在屏幕上显示的字符。那么第226行的比较结果肯定不是“0”,ZF被清零。第227行的跳转条件成立。程序跳转到第231行继续执行,如下图:
在正常显示读取字符的情况下,处理器直接显示该字符。
第234行,由前面运行可以知道此处BX的值为光标位置数据。“shl bx,1”是将bx逻辑左移以为,相当于bx×2。这个值就是光标在显存中的偏移地址。
第235行,此时CL的值为取到的字符的ASCII码值,将此值给“”,字符将在屏幕上显示。为什么不在后面紧接着显示字符的颜色属性呢,因为在写入其他内容之前,显存里全部是黑底白字的空白字符,所以不需要重写黑底白字的属性。
执行完第235行也就是说显示完取到的字符之后,光标当然要往前推进一个位置,以表示下一个字符要显示的位置。前面第234行已经将bx的值逻辑左移了一位,这里再逻辑右移一位,恢复bx原来的值。第239行,将bx的值加1,表示光标位置到了下一个字符的位置。这里第238、239行的bx的值均为光标位置数据。
执行完第239行之后,光标换了新的位置,当然又要考虑是否需要滚屏等等之类的因素,如果需要滚屏则滚屏,如果不需要滚屏就设置光标到新的位置。
经过上面一系列的处理,直到最后取到“0”,标号“init_msg”处的“'Starting...',0x0d,0x0a,0”整个字符串显示完毕,过程“put_string”执行完毕,程序返回到过程的下一行第129行。如下图:
这两行同第126行、第127行一样,也是显示一个字符串信息。只是参数不一样,显示的是标号“inst_msg”处的“'Installing a new interrupt 70H...',0”字符串。工作过程和前面一样,调用的都是同样的过程,这里不再重复。
接——X86汇编语言-从实模式到保护模式—学习笔记(十三)
页:
[1]