30_OS 第2天 汇编语言学习与Makefile入门

第2天 汇编语言学习与Makefile入门

2019.12.28-2019.12.29

1. helloos.nas节选详解

  • helloos.nas

    ; hello-os
    ; TAB=4
    
    	  ORG		0x7c00			; 指明程序的装载地址
    
    ; 以下的记述用于标准的FAT12格式的软盘
          JMP     entry
          DB      0x90
    ---(中略)---
    
    ; 程序核心
    
    entry:                          ; 0x7c50
    	  MOV		AX,0			; 初始化寄存器
    	  MOV		SS,AX
    	  MOV		SP,0x7c00
    	  MOV		DS,AX
    	  MOV		ES,AX
    
    	  MOV		SI,msg
    putloop:
    	  MOV		AL,[SI]
    	  ADD		SI,1			; 给SI加1
    	  CMP		AL,0
    	  JE		fin
    	  MOV		AH,0x0e			; 显示一个文字
    	  MOV		BX,15			; 指定字符颜色
    	  INT		0x10			; 调用先看BIOS
    	  JMP		putloop
    fin:
    	  HLT						; 让CPU停止,等待指令
    	  JMP		fin				; 无限循环
    
    msg:                            ; 0x7c74
    	  DB		0x0a, 0x0a		; 换行2次
    	  DB		"hello, world"
    	  DB		0x0a			; 换行
    	  DB		0
    
    • ORG指令:这个指令会告诉nask,再开始执行的时候,把这些机器语言指令装载到内存的哪个地址。如果没有它,有几个指令就不能正确地被翻译和执行。另外,有了这条指令的话,美元符$的含义也随之变化,它不再是输出文件的第几个字节,而是代表将要读入的内存地址
      ORG指令源自英文origin。它会告诉nask,程序要从指定的这个地址开始,也就是要把程序装载到内存中指定的地址
      这里指定的地址为0x7c00。这是规定,原因和1+1为什么等于2一样。

    • JMP指令:来源于英文jump,相当于C语言的goto语句。

    • entry:这是标签的声明,用于指定JMP指令的跳转目的地等。

    • MOV指令:最常用的指令。功能:赋值。虽然简单,但是要是完全理解了MOV指令,汇编语言也就理解了一大半了。

      • MOV AX,0 相当于AX=0MOV SS,AX相当于SS=AX后者赋值给前者
      • MOV指令的另一种理解是COPY。执行MOV SS,AX以后AX的值并没有空,还是保持原来的值不变。
    • CPU中的寄存器

      • AX:accumulator,累加寄存器
      • CX:counter,计数寄存器
      • DX:data,数据寄存器
      • BX:base,基址寄存器
      • SP:stack pointer,栈指针寄存器
      • BP:base pointer,基址指针寄存器
      • SI:source index,源变址寄存器
      • DI:destination index,目的变址寄存器

      以上寄存器都是16位寄存器,因此可以存储16位的二进制数。
      CX是为方便计数而设计的;BX适合作为计算内存地址的基点。
      另外,CPU中还有8个8位寄存器:

      • AL:累加寄存器低位(accumulation low)
      • CL:计数寄存器低位(counter low)
      • DL:数据寄存器低位(data low)
      • BL:基址寄存器低位(base low)
      • AH: 累加寄存器高位(accumulation high)
      • CH:计数寄存器高位(counter high)
      • DH:数据寄存器高位(data high)
      • BH: 基址寄存器高位(base high)

      AX寄存器有16位,其中0~7位的低8位称为AL,8~15位的高8位称为AH
      因此,CPU还是那个CPU,依然只能存储16个字节。CPU的存储能力真的是太有限了。

      为什么BP、SP、SI和DI没有分H和L呢?
      比如,要分别取SI的高8位和低8位。嘤特尔的设计人员是这样想的:先用> MOV AX,SI,将SI赋值到AX里面去,再用AH和AL取值。

      32位CPU寄存器

      • EAX
      • ECX
      • EDX
      • EBX
      • ESP
      • EBP
      • ESI
      • EDI

      EAX的低16位是AX,高16位没有名字也没有寄存器编号。因此,我们可以把EAX当做2个16位寄存器来用,但是只有低16位用起来方便;我们想要用高16位的话,需要使用移位命令,将高16位移到低16位使用。
      32位CPU的存储能力也只有32字节,同样小的可怜。
      对于64位CPU,我们这次不使用64位模式。

      段寄存器

      ES: 附加段寄存器(extra segment)
      CS: 代码段寄存器(code segment)
      SS: 栈段寄存器(stack segment)
      DS: 数据段寄存器(data segment)
      FS: 没有名称(segment part 2)
      GS: 没有名称(segment part 3)

      以上段寄存器都是16位寄存器

    • MOV SI,msg
      意思是SI=msgmsg是此行代码下面即将要出现的标号。

      这句话的意思是将标号赋值给寄存器么?

      前面有JMP entry,其实将它写成JMP 0x7c50是一样的。因为entry代表的就是0x7c50。这句指令的意思就是让CPU去执行内存地址为0x7c50的程序。

      在汇编语言中,所有的标号仅仅都是单纯的数字。
      每个标号对应的数字,都是由汇编语言编译器根据ORG指令计算出来的。

      ORG指令:这个指令会告诉编译器,在开始执行的时候,把这些机器语言指令装载到内存的哪个地址。

      编译器计算出来的“标号的地方对应的内存地址”就是那个标号的值。

      PS: 可以理解成,标号就是一个数字,这个数字是内存地址。因此,标号就是内存地址。

      在这里msg的地址是0x7c74,因此MOV SI,msg的含义就是,把内存地址0x7c74放入SI寄存器中。

    • MOV AL,[SI]

      在汇编语言中,这个[]代表内存

      CPU的存储能力很差,因此想要让CPU处理大量的信息,就得必须给CPU另外准备一套用于存储的电路。
      因为即便是32位的CPU,把所有的普通寄存器都加在一起,最多也只能存储32个字节,就算把所有的段寄存器也都用上,也才只有44个字节(6个16位的段寄存器,共12字节,加上普通寄存器32字节,共44字节)。

      内存

      • 内存是CPU的外部存储器。
      • CPU通过自己的一部分管脚(引线)向内存发送电信号,以读写内存中的数据。(严格来讲,CPU和内存之间还有称为芯片的控制单元)
      • CPU与内存之间的电信号交换,并不仅仅是为了存取数据。因为从根本上来讲,程序本身也保存在内存里。程序一般都大于44字节,因此不可能存储在CPU中,所以规定程序必须放在内存里
      • CPU在执行机器语言时,必须从内存中一个命令一个命令地读取程序,顺序执行
      • CPU访问内存的速度比访问寄存器的速度慢好几倍。记住这一点,我们才能开发出执行速度快的程序来。

      MOV指令的数据传送源和传送目的地不仅可以是寄存器或常数,也可以是内存地址。这个时候,我们使用方括号[]来表示内存地址。

      举个栗子
      MOV BYTE [678], 123
      这个指令是要用内存的“678”号地址来保存123这个数值。虽然指令里面有数字,看起来像那么回事,但实际上内存和CPU一样,根本没有什么是数值的概念。所谓的“678”不过就是一大串开(ON)或者关(OFF)的电信号而已。当内存收到这样的一串电信号时,电路中的某8个存储单元就会响应,这8个存储单元会记住代表“123”的开(ON)或关(OFF)的电信号。为什么是8位呢?因为指令里制定了“BYTE”(汇编语言保留字)。同样我们还可以写成:
      MOV WORD [678],123
      在这种情况下,内存地址中678号和旁边的679号都会做出反应,一共是16位。这时,123会被解释成为一个16位的数值(WORD是保留字,16位),也就是0000000001111011,低位的01111011保存在678号,高位的00000000保存在旁边的679号。

      高位高地址:小端模式
      大小端模式

      数据大小 [地址]
      这时一个固定的组合。如果我们指定“数据大小”为BYTE,那么使用的存储单元就只是地址所指定的字节。如果我们指定数据大小为WORD,则相邻相邻的意思是指地址增加方向的相邻)的一个字节也会变成这个指令的操作对象。如果指定DWORD,则与WORD相邻的两个字节(加上这两个,共四个字节)也都会成为指令的操作对象。
      至于内存地址的指定方法,我们不仅可以使用常数,还可以使用寄存器

      比如BYTE [SI],如果SI中保存的是987,那么BYTE [SI]会被解释成BYTE [987],即指定地址为987的内存。

      虽然我们可以使用寄存器来指定内存地址,但是只有BXBPSIDI。剩下的AXCXDXSP都不能用来指定内存地址(嘤特尔大叔们的奇怪设计)。

      因此,我们想把DX内存里的内容赋值给AL的时候,应该这样写:

      MOV BX, DX  
      MOV AL, BYTE [BX]  
      

      根据以上,我们可以用下面的指令将SI地址的1字节内容写到AL中。
      MOV AL, BYTE [SI]
      同时,MOV指令有一个规则,那就是源数据和目的数据的位数必须相同。也就是说,能向AL里带入的只有BYTE,这样可以改写成:
      MOV AL, [SI]
      这个指令的意思是:SI地址的一个字节的内容读入AL
      这个规则的含义是:
      [SI]的含义是一个地址,这个地址是8位的。AL也是8位的,所以是可以简写的。但是,MOV AX, CL就会编译出错,AX是16位,CL是8位。

    • ADD SI, 1
      ADD是加法指令。这句指令的意思是:SI = SI + 1,即让SI自加1。

    • CMP AL, 0
      CMP是比较指令。这句指令的意思是将AL中的值和0进行比较。一般来讲,CMP指令的后面都会有JE或其他指令。

    • JE fin
      JE指令是跳转指令之一。

      跳转指令:根据比较的结果决定跳转或者不跳转。

      JE指令:如果比比较结果相等,就跳转;如果比较结果不等,就不跳转,继续执行下一条指令。
      因此,

      CMP AL, 0
      JE fin  
      

      这两条指令在一起相当于,

      if (AL == 0){
        goto fin;
      } 
      

      其实,JE指令来源于英语“Jump if Equal”,意思是如果相等就跳转。
      fin是一个标号,表示结束(finish)的意思。

    • INT 0x10
      INT指令是中断指令。中断指令以后再讲(PS:挖坑)。

      电脑里有个名叫BIOS程序,出厂时就组装在电脑主板上的ROM单元里。

      ROM:只读存储器,"Read Only Memory"的缩写。**切开单电源以后内容不会消失。

      电脑厂家在BIOS中预先写入了操作系统开发人员经常会用到的一些程序,非常方便。
      BIOS的全称是"Base Input Output System"。直译过来就是“基本输入输出系统(程序)”。
      现在电脑的BIOS的功能非常多,甚至包括了电脑设定的画面。不够它的本质正如其名,就是为操作系统开发人员准备的各种函数的集合

      INT指令就是用来调用这些函数的指令。INT指令后面紧跟着一个数字,使用不同的数字可以调用不同的函数。比如,0x10(即16)号函数,它的功能是控制显卡

      关于BIOS的网址 PS:不知为啥访问不了qwq

      比如,现在要显示文字,假设先显示一个字。
      首先,显示文字,及得查看你与显卡有关的函数,即16号函数。查找有关16号函数的信息:

      显示一个字符

      AH = 0x0e; 
      AL = character code;
      BH = 0;
      BL = color code;
      

      无返回值
      注:beep退格(back space)CRLF都会当成控制字符处理。

      现在,AH=0x0e,因为有代码:MOV AH, 0x0e
      AL=[SI],因为有代码:MOV AL, [SI]
      BH=0BL=15因为有代码:MOV BX, 15

    • HLT
      HLT是“halt”的缩写,意思是停止。HLT指令是让CPU停止动作的指令,不过不是彻底停止(如果要彻底停止CPU的动作,只能切断电源),而是让CPU进入待机状态。只要外部发生变化,比如按下键盘,或是移动鼠标,CPU就会醒过来,继续执行程序。

      仔细观察程序,就会发现,其实有没有HLT指令,也是进入无限循环。
      但是,如果没有HLT指令,那么CPU就会不停地去执行JMP指令,让CPU的负荷达到100%。因此,使用HLT指令,既环保又节约电费(PS:手动狗头)。

      (PS:终于把这个程序从头到尾讲完了)

  • 用C语言改写之后的helloos.nas程序节选

    entry:
    	AX = 0;
    	SS = AX;
    	SP = 0x7c00;
    	DS = AX;
    	ES = AX;
    	SI = msg;
    
    putloop:
    	AL = BYTE [SI];
    	SI = SI + 1;
    	if (AL == 0){
    		goto fin;
    	}
    	AH = 0x0e;
    	BX = 15;
    	INT 0x10;
    	goto putloop;
    
    fin:
    	HLT;
    	goto fin;
    

    有了这个程序,我们才能把msg中写入的数据,一个字符一个字符地显示出来,并且当数据编程0的时候,HLT命令就会让程序进入无限循环。

  • 神奇的0x7c00

    内存中的所有地址我们都可以用么?

    显然,不可能。比如,内存的0号地址,也就是最开始的地方,是BIOS程序用来实现不同功能的地方,如果我们使用的话,就会和BIOS发生冲突,这时,结果不但是BIOS会出错,我们的程序也会出错。另外,在内存的0xf0000号地址附近还放着BIOS程序本身,那里我们也不能使用。
    内存中有不少地方是不能用的。详见"内存地图"PS:不知道为啥访问不了
    其中,
    0x00007c00-0x00007dff:启动区内容的装载地址。
    0x00007c00是嘤特尔大叔们规定的。只有程序的装载地址是0x7c00的时候,我们的程序才能正确地启动。

2. 制作启动区

  • projects/02_day的helloos4文件夹
    考虑到以后的开发,我们不要一下纸就用nask来做整个磁盘映像,而是先只用它来制作512字节的启动区,剩下的部分我们用磁盘映像管理工具来做,这样以后用起来就方便了。
    首先,我们把helloos.nas的后半部分裁掉了,这是因为启动区只需要最初的512字节。现在这个程序就仅仅是用来制作启动区的,所以我们把这个文件名也改成了ipl.nas
    ipl.nas

    asm.bat

    ..\z_tools\nask.exe ipl.nas ipl.bin ipl.lst
    

    making.bat

    ..\z_tools\edimg.exe   imgin:../z_tools/fdimg0at.tek   wbinimg src:ipl.bin len:512 from:0 to:0   imgout:helloos.img
    

    making.bat的大致意思是:用磁盘管理工具edimg.exe,先读入一个空白的磁盘映像文件,然后在开头写入ipl.nas的内容,最后将结果输出为名为helloos.img的磁盘映像文件。
    这样,从编译到测试的步骤就变得非常简单了:打开终端,依次输入asm.bat->making.bat->run.bat

    helloos4文件夹

    打开终端进入目录,依次输入命令:

    会看到在QEMU中运行:

3. Makefile入门

  • Makefile的写法
    首先生成一个不带拓展名的文件Makefile,然后用文本编辑器写入如下内容:

    ipl.bin : ipl.nas Makefile
    		../z_tools/nask.exe ipl.nas ipl.bin ipl.lst
    helloos.img : ipl.bin Makefile
    	../z_tools/edimg imgin:../z_tools/fdimg0at.tek \
    	wbinimg src:ipl.bin len:512 from:0 to:0 imgout:helloos.img  
    
    • # 代表注释。

    • ipl.bin : ipl.nas Makefile的意思是,如果想要制作文件ipl.bin,就先检查一下ipl.nasMakefile这两个文件是否都准备好了。如果这两个文件都有了,Make工具就会自动执行Makefile的下一行。

    • 同理,helloos.img的生成方法类似,其中\的含义是续行符号,表示这一行写不下了,跳到下一行继续写。

    • 我们需要调用工具make.exe来让这个Makefile发挥作用。为了方便地从命令行窗口运行这个工具,我们需要写一个make.bat

      make.bat

      ..\z_tools\make.exe %1 %2 %3 %4 %5 %6 %7 %8 %9   
      
    • 打开命令行,然后输入make -r ipl.bin,这样make.exe就启动了,它首先读取Makefile文件,寻找制作ipl.bin的方法。因为ipl.bin的制作方法就写在Makefile里面,make.exe找到了这一行就去执行其中的命令,顺利生成ipl.bin。然后我们再输入make -r helloos.img,同理,会启动make.exe,并按照的Makefile中制作helloos.img的方法去执行对应的命令,最终成功生成helloos.img

    • 我们把ipl.binhelloos.img删除以后。在命令行输入make -r helloos.imgmake首先很听话地去试图生成helloos.img,但是发现ipl.bin还不存在,于是make又去找生成ipl.bin的方法,找到以后,先生成ipl.bin然后又生成helloos.img。(PS:睿智的make)

    • 下面,我么在此基础上不删除任何文件,在命令行输入make -r helloos.img,就会发现,命令行输出为:

      会有'helloos.img' is up to date.的输出,什么命令都不会执行,也就是说,make知道helloos.img已经存在,没必要重新做一遍了。(PS:睿智的make*2)

    • 下面,我们编辑ipl.nas的内容,把它改成任意你喜欢的内容并保存起来。而ipl.nas和helloos.img保持原来的样子不变,在这种情况下,我们再次执行代码make -r helloos.img。结果发现,make.exe又重新开始生成输出文件。也就是说,make.exe不仅仅判断输入文件是否存在,还会判断文件的更新时间,并根据此来决定是否重新生成输出文件。(PS:睿智的make*3)

    • helloos5文件夹下的Makefile

      default :
      ../z_tools/make.exe img
      
      ipl.bin : ipl.nas Makefile
      ../z_tools/nask.exe ipl.nas ipl.bin ipl.lst
      
      helloos.img : ipl.bin Makefile
      	../z_tools/edimg.exe   imgin:../z_tools/fdimg0at.tek \
      	wbinimg src:ipl.bin len:512 from:0 to:0 \
      	imgout:helloos.img
      
      asm :
      	../z_tools/make.exe -r ipl.bin
      
      img :
      	../z_tools/make.exe -r helloos.img
      
      run :
      	../z_tools/make.exe img
      	copy helloos.img ..\z_tools\qemu\fdimage0.bin
      	../z_tools/make.exe -C ../z_tools/qemu
      
      install :
      	../z_tools/make.exe img
      	../z_tools/imgtol.com w a: helloos.img
      
      clean :
      	-del ipl.bin
      	-del ipl.lst
      
      src_only :
      	../z_tools/make.exe clean
      	-del helloos.img
      

      输入make img 即可生成helloos.img
      输入make run 即可生成相应文件,并出现QEMU
      输入make clean 即可删除ipl.binipl.lst
      输入make,即不带参数的make,执行默认(写在第一行),即执行make img

    • PS:make yes!

4. 今天是我的好哥们SX的生日,今晚我们大家吃了蛋糕,祝他生日快乐!


版权声明:本文为Histranger_原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。