Skip to main content
A cup of beer
  1. Posts/

汇编语言学习笔记-ch1-4

·248 words·2 mins
Table of Contents
汇编语言的构成 #
  • 汇编指令:机器指令的别名。因此一条汇编指令就一一对应了一条机器指令。
    • 汇编指令是汇编语言的主要组成部分。
  • 伪指令:没有对应的机器指令。其只是被编译器执行的指令,而CPU看不懂。
  • 其他符号:例如+、-、*、/。编译器识别并根据此进行一些编译。
接口卡 #
  • 接口卡是真正直接控制外设的部件。接口卡插在扩展插槽上,通过总线链接CPU。因此CPU是通过给接口卡发送命令从而间接控制了外部设备。
BIOS #
  • 只要是独立的部件就可以有BIOS(Basic InputOutput System)。
  • BIOS由厂商提供,实现此设备的最基本的输入输出的控制。例如显卡有显卡BIOS,网卡有网卡BIOS。
  • BIOS一般存在ROM中。例如主板上有一个ROM专门存放主板BIOS。
  • 无论是哪个设备的BIOS,由于这些设备都通过总线链接CPU,所以CPU的地址空间中都有存放这些BIOS的ROM的一份。因此可见CPU的地址空间并不完全由内存构成,此外还由若干外设构成!!虽然都是外设,但是CPU可以直接把他们都当作内存来看待。
显卡 #
  • 显卡随时把显存中的数据往屏幕上送。
  • 把东西放到显存中,就能实时显示在屏幕上。
8086的字节和字 #
  • 为了对上一代CPU有更好的兼容性,8086把上一代CPU能处理的最大位数(8bits)称为一个字节,把自己能一次性处理的最大位数(16bits)称为一个字。
  • 因此,最初对于字和字长的定义就是为了8086CPU可以兼容为上一代CPU编写的程序。
8086的通用寄存器 #
  • 通用寄存器就是存放==一般数据==的寄存器。以X结尾。在8086中记作AX,BX,CX,DX。
  • 8086是16位的。为了兼容为上一代CPU编写的程序,8086把自己的寄存器划分为可以支持两个8位存储的“虚拟的小寄存器”,以AX为例,记作AL、AH。
    • 注意,如果在汇编指令中使用AL、AH,而不是AX,例如add AL,9999H,则结果只是会在AL中溢出,即最高位丢失,而不会把进位保存在AH中。因为在这种情况下,AH和AL看作两个相互独立,没有任何联系的寄存器。
  • ==不允许==在AX和BL等类似于这样的位数不同的寄存器之间执行MOVADD等操作。
16位机是什么意思? #
  • 寄存器都是16位的
  • 运算器一次可以处理16位的数据
  • 寄存器和运算器之间的通路是16位的

即:Reg和ALU是16位的且他们之间用16位连接。

为什么要段地址左移4位+偏移地址来形成物理地址? #
  • 8086是16位机,所以CPU只能处理16位的数据。但是8086的IO有20根数据线,希望能在16位机的条件下,达到20位地址的寻址能力。
怎么形成20位地址,20位地址怎么给了内存? #
  • CPU提供16位段地址和16位偏移地址,都送入地址加法器。
    • 注意:因为16位的限制,所以一个段的大小==最大只能是64KB==。
  • 地址加法器将段地址左移四位,加偏移地址,获得物理地址。(==即由段地址和偏移地址获取了物理地址==)
  • 物理地址从地址加法器送到IO控制电路,传递给内存。
段起始地址(基础地址)、段地址的区别 #
  • 段起始地址是一个物理地址。
  • 段地址是逻辑地址。
  • 它们的关系是:后者左移4位得到前者。
    • 因此段起始地址必然是16的倍数。
8086的段寄存器 #
  • 存放段地址的寄存器称为段寄存器,无论这个段是什么段。
  • 8086有4个段寄存器,都以S结尾,分别是CS代码段寄存器、ES附加段寄存器(可以看作是通用段寄存器,如果实在没有段寄存器可用了就用它)、DS数据段寄存器、SS栈段寄存器。
CS:IP和指令执行的关系 #

IP(Instruction Pointer)指令指针寄存器。

  • CS:IP指向的是CPU要执行的指令。
    • 因此,可以通过是否被CS:IP指向判断这里存的是指令还是数据。
    • 如果一条指令被执行了,那么它一定被CS:IP指向过。
    • 8086在加电或重置后,CS:IP被设置为FFFFH:0000H。这是8086CPU在开始运行时执行的第一条指令。
  • IP指向下一指令的过程
    • CS:IP的指令取出后,放入IR,分析此指令的长度
    • 根据上述指令的长度,==让IP+这个长度==
    • 重复上述步骤
如何手动修改CS:IP的值? #
  • 8086CPU不提供使用MOV指令直接给CS或IP寄存器赋值的方式.例如MOV CS,3是不合法的.
  • JMP指令有两种用法:
    • JMP数字,可以同时给CS:IP赋值。例如JMP 32:14相当于MOV CS,32 MOV IP,14的功能。(只是用来解释。实际上后者不合法。)
    • JMP寄存器,可以只给IP赋值。例如JMP AX,就相当于MOV IP,CS.
    • 因此,==JMP指令可以说是以特定的方式代替了MOV给CS或者IP赋值的操作==。
字单元和内存单元 #
  • ==8086的内存是以字节编址的==,所以一个内存单元就是一个字节的大小,8bits。
  • 8086的字长是16bits,所以一个字单元应包含两个内存单元。
    • 如果要存储一个字,那么字的高8位存在较高的那8位地址中(即起始地址较高的内存单元中),字的低8位类似。
    • 字单元按其起始内存单元的编号来编址,即其低8位所在内存单元的地址就是整个字单元的地址。例如我们说地址为2的字单元,就是指由地址2、地址3构成的字单元。
  • 因此,任意两个连续的内存单元,可以看成是两个独立的内存单元,也可以看成是以较低的那个内存单元为地址的字单元。
如何利用DS以正确读取数据? #
  • ==8086的数据读取只能以偏移地址的方式读取。==因为8086会默认地从DS中取段地址并且和偏移地址合成物理地址。

    • 因此,需要正确设置DS寄存器的值才能正确读取数据。
  • 如何设置DS寄存器的值?

    • 由于DS寄存器属于段寄存器,而8086的硬件设计不允许直接对段寄存器进行MOV操作,所以需要使用中转寄存器(任意一个通用寄存器即可)。例如:

    • mov bx,1000H
      mov ds,bx
      mov al,[1]  //成功读取到了10001H的内存单元的数据,送入al寄存器。
      //其中,中括号表示内存单元,其中的1表示偏移地址。
      
  • 对字节的操作和对字的操作(8086作为16位机,支持一次性读取一个字的操作):如果从内存单元mov到一个8位寄存器,那么读取这个地址的内存单元;如果从内存单元mov到一个16位的寄存器,那么读取==这个地址的字单元==(例如到AX,则AH和这个字单元的高8位对应,AL和这个字单元的低8位对应)。

8086的栈 #
  • 8086和很多CPU一样,都提供==以栈的方式来==访问内存空间的指令。
    • 因此,所谓栈,实际上是访问内存空间的一种方式,即以LIFO的方式对内存进行访问。这段内存表现在8086中,就是一个“segment”。
  • 注意,8086中,栈的压入和弹出是==以字为单位==进行的。因此,对于一段栈内存空间,其中的存储都是==字单元==。
  • 8086无法对栈进行越界的检查。因此如果出现了栈溢出,可能会损害整个计算机系统。(我们既然把一段内存安排为栈段,就说明我们肯定不会用到其他空间,因此其他空间可能跑着其他程序的重要数据,甚至是OS。)
    • 8086只知道栈顶在哪里,却不知道栈的大小是多少。
    • 同样的原理,8086只知道要执行的指令在哪里,但是不知道一共有多少指令。
  • 在利用栈编写汇编程序时,要注意栈有将数据==反向排列==的功能。因此入栈和出栈的顺序是相反的。
  • 像定义代码段那样,我们可以把起始物理地址为==16的倍数==,且==连续的==不超过==64KB==的一段内存空间定义为栈段。
  • 如何让我们的程序访问我们的栈段?需要我们自己设定SS、SP寄存器的值。因为8086 CPU==不会==因为我们安排了一段内存空间为栈段,就自动寻找到它。
SS和SP寄存器 #
  • SS:SP指向栈顶元素。SS是段地址寄存器,SP是栈顶指针寄存器。

  • 在8086中,栈底是较大的地址号,栈顶是较小的地址号。因此如果把内存空间按地址编号从小到大向下排列的方式,则栈顶才会看起来是内容越多,其越往上走;随着内容的逐渐弹出,它才会下降。但是实际上从编号上来看,栈顶上升实际上是内存编号减小。

    • 又因为8086中的栈是以字为单位进行操作,所以入栈操作时==SP-2==.
  • 当栈为空(或者说是刚刚定义),SS指向栈的==最顶端==,然后再右移四位(如果栈超过了这里那就是爆栈了,也就是SP为负了),SP指向栈的==最底栈空间的再下一个==字单元(如果该单元不存在,也就是如果特殊情况,该栈空间到最底下就到了整块内存的最底处了,那么也得这么做。例如CPU的最大地址空间到了FFFF1H,那么如果栈空间最底部的元素的地址就是FFFF1H,根据规则,栈空时,SP应该指向此地址再+2,即FFFF3H,显然该地址在物理上不可能存在,但是写在这里是没有任何问题的。)(+2是因为我们采用字单元的地址,但是8086采用按字节编址。

  • 由上述空栈时SP和SS的取值关系,可以看出==SP的初始值直接就定义了整个栈的大小。==例如一个16B的栈空间,那么不管SS是什么,其SP一定是000EH(即000FH-1H,因为我们采用字单元地址。)

  • 出栈操作并不会抹除原有的数据,而只是移动一下SP的值就完成了整个出栈操作。因为下次有值入栈的时候,可以直接覆写对应的单元。因此可以看出抹除数据是没有任何意义的。

    • 这就类比C++ iterator,当把vector的最大下标元素删除后,使用下标仍然能访问到,且其值并没有任何变化。因为也是采用了类似原理。
“盲人大厨”CPU #
  • 我们可以自行安排段,安排我们的数据段、代码段、栈段。但是这和CPU没有关系。你不能用自然语言告诉CPU你要把这些段安排在哪里。你告诉它的方式是:
    • 我们设置CS、IP的值,告诉CPU代码段在这里,你可以把它们当作代码执行了;
    • 我们设置SS、SP的值,告诉CPU栈段在这里,你可以按照栈的方式访问这段内存了;
    • 我们设置DS的值,告诉CPU代码段在这里,你可以正常访问属于你的内存了。
    • CPU就像一个盲人厨师,什么都懂,就是看不见调料、锅、菜在哪里。你要拿着他的手,让他知道这些东西在哪里,他就记住了。然后他就能非常自如地炒出一盘好菜。但是如若你没有准确地告诉他正确的位置,那他可能把勺子炒了。(CPU自己不知道栈段在哪里,但是他知道应该SS和SP指向的就应该按照栈的方式访问;CPU自己不知道代码段在哪里,但是他知道CS、IP指向的就是要执行的东西,…,重要的是你要告诉CPU这些东西。)
  • 因此,就算你把代码段、数据段、栈段都放在同一段内存里,CPU自己也不知道。==因此你可以这样来调戏CPU(bushi)==
程序如何从源文本文件变成可执行程序? #
  • 编译(汇编):生成目标文件.o
  • 连接:==生成可执行文件==。
    • 如果源程序由很多.obj文件构成,则需要把他们连接成单个可执行文件;
    • 如果源程序调用了很多外部库,则需要把他们整合进最终的可执行文件;
    • .obj文件中虽然存放了机器码,但是其中的一些内容还不能直接用来运行。需要经过连接程序处理后,生成最终可供直接运行的文件。(因此,即便没有调用外部库,即便没有采用多个源文件,也需要进行连接。)
      • 连接器将.o中定义的各个section组织到内存中的segment中去。其中section就是例如data section、ubuntu info section,是一些小片段,只是.o文件的单位。而segment是之前所讲的代码段、数据段等,是内存中的单位了。
可执行文件的构成 #
  • 可执行文件在总体上由头、代码、数据构成。
    • 头中存在了关于程序的描述,例如占用大小、二进制代码的排列等
    • 程序就是机器码,是经过汇编得到的。
    • 数据是源程序中预先定义的数据。
  • OS把CS:IP指向程序开始,并且把数据、程序按其段装入内存。CPU直接取指令执行即可。
编写一个汇编程序 #
  • 伪指令:不存在对应的机器码,只是供编译器进行识别,指导编译器进行编译操作。

    • XXX segmentXXX ends声明了一个段。它们配对构成,在它们之间书写段的内容。(注意它们两个都是后缀。即先写段名称再写关键字)

      • 一个程序由若干个段构成,每个段有其自己的作用。一个程序至少要由一个代码段构成才叫程序。不存在无段的程序。

      • 例如:

        • codesg segment
          //codesg段内容
          codesg ends
          
    • end用来标志整个程序的结束。编译器读取到end才知道可以结束工作了,否则编译器无法退出。

    • assume Reg:XXX的意思是假设,用于建议编译器把某个段XXX和物理的段寄存器Reg相关联。在需要的情况下,编译器会采纳这一建议,并真的把它们关联起来。例如:assume cs:codesg就是把codesg这一代码段和cs程序段寄存器关联起来了。

      • 可以在声明一个段之前使用assume指令去关联这个段使之==有效化==。例如:

        • assume cs:codesg //有效化这个段
          codesg segment
          //codesg段内容
          codesg ends
          
  • 标号

    • 标号代表了一个地址。例如上面所说的代码段名称codesg就是一个标号。
    • 虽然在源代码中体现不出是短地址,但是编译器、连接器经过处理,可以把它处理为一个地址,也就是代码段的段地址。
  • 程序的结构

    • ==程序就是一些段的集合。== 至少要包含一个段,那就是代码段。
  • 编写程序的流程

    • 首先定义一个段。因为程序的基本单位是==段==。

      • abc segment
        abc ends
        
    • 然后,==补充==这个段的内容。这个段是代码段,所以写入程序。

      • abc segment
        	add ax,bx
        	mov ax,2
        abc ends
        
    • 然后,==有效化==这个段。

      • assume cs:abc
        abc segment
        	add ax,bx
        	mov ax,2
        abc ends
        
    • 最后,指出程序在何处==结束==。

      • assume cs:abc
        abc segment
        	add ax,bx
        	mov ax,2
        abc ends
        end
        
  • 程序执行

    • 在DOS系统中,我们运行一个可执行文件,则CPU控制权由DOS系统交给了此程序(因为DOS是单任务操作系统)。
      • 具体地说,DOS启动后,完成各项准备工作,就开始运行command.com程序。这就是DOS 的shell。
      • 我们键入可执行文件的名称,command程序就把可执行文件的程序装入内存,设置CS:IP,然后CPU执行它,同时command停止执行,装入的程序开始执行。直到装入的程序执行完毕,command才开始继续执行。
      • debugcommand的区别是:debug并不放弃对CPU的控制权。因此当其装入的程序开始执行时,debug仍然保持对CPU的控制。这就允许我们能够单步调试目标程序了。
      • 我们输入debug a.exe,实际上是command装入了debugdebug又装入了1.exe。因此1.exe执行完毕后,先返回到debug,再返回到command。如果我们在1.exe执行完毕后不给debug退出的命令,你会发现你仍然一直处于debug的命令提示符里面。
      • 程序装入后,CX寄存器中的值代表了程序的大小(即==长度==)。
      • 程序装入后,DS寄存器中的值是sa,也即是程序被分配的内存区的所在段地址(因此,程序被分配的内存的起始物理地址就是DS:0)。
        • 在sa:0到sa:00FFH这段256个B(即从0000H到00FFH,十进制0到255)的空间中,存放PSP,也就是DOS系统中的程序段前缀。
        • 因此,程序段地址实际上是sa+10H(因为10H左移4位是100H也就是256),程序从sa+10H:0开始。
        • 确定后,将sa存入DS寄存器,然后设置CS:IP到sa+10H==即可完成程序的装入,完全可以直接开始执行了==。
  • 程序返回

    • 当此程序运行完毕,应该再交还此CPU控制权。这个交还的过程就是==返回==。

    • 注意在debug时,单步执行其他指令用t,但是执行int 21H时必须用p

    • mov ax,4c00H //中断参数
      int 21H //21H中断:返回DOS系统