美文网首页
《程序是怎样跑起来的》(下)

《程序是怎样跑起来的》(下)

作者: c747190cc2f5 | 来源:发表于2019-05-05 16:21 被阅读0次

    学习笔记

    第8章 从源文件到可执行文件

    本章问题:

    问题

    本章重点:

    编译器的功能;程序从源代码到可执行文件的流程;程序运行时的堆和栈。


    8.1 计算机只能运行本地代码

    一个例子1
    一个例子2
    图中栗子的源代码文件命名为Sample1.c。
    源代码需要转换成本地代码才能运行

    8.2 本地代码的内容

    直接用记事本打开本地代码:

    记事本打开本地代码
    把本地代码Dump一下,每一个字节用2位16进制数(每个16进制数代表4位二进制数,2位16进制数恰好代表8位即1字节)来表示:
    本地代码的真是面目是数值的罗列

    8.3 编译器负责转换源代码

    • 不同的语言有各自的编译器;
    • 不同类的CPU有不同的机器语言,需要不同的编译器;
    • 编译器也是应用程序,也需要运行环境;
    • 存在交叉编译器,在某一环境下运行,可以生成另一环境下的本地代码。
    通过命令行编译上面的栗子

    8.4 仅靠编译是无法得到可执行文件的

    • 编译生成的是.obj文件(目标文件),而不是.exe文件,无法直接运行;
    • 如果源代码中引用了其它的函数(如上面例子中的sprintf()、MessageBox()),就需要把储存着这些函数的目标文件与此目标文件相结合;
    • 完成此工作的是链接器,最后生成.exe文件
    链接上面的栗子

    8.5 启动及库文件

    • 在链接的命令中,c0w32.obj记述的是同所有程序起始位置相结合的处理内容,称为程序的启动,即使未调用其它目标文件的函数,也必须要进行链接,并和启动结合起来;
    • 扩展名为.lib的文件称为库文件,是多个目标文件的集合。链接器指定库文件后,会把需要的函数从库文件中提取出来,例子中的sprintf()储存在cw32.lib中,MessageBox()实际上是储存在user32.dll中(会在后面说明原因);
    • 库文件可以简化链接过程,当需要很多个库函数时,只需要链接数个库文件就可以了;
    • sprintf()等函数,不通过源代码而是通过库函数和编译器一起提供,称之为标准函数,标准函数以目标文件形式集合在库文件中,不会暴露源码,可以避免造成商业损失。

    8.6 DLL文件及导入库

    • Windows以函数的形式为应用提供了各种功能,称之为API(Application Programming Interface),上面的栗子中MessageBox()就是Windows提供的一种API;
    • Windows中的API并非储存在通常的库函数中,而是储存在DLL(动态链接库)中,DLL是程序运行时动态结合的文件;
    • 与DLL相反,存储目标文件的实体,并直接与EXE结合的称之为静态链接库,例如cw32.lib;
    • 通过导入库文件,EXE在执行时会从DLL调出函数的信息就会写在EXE中。

    用下图总结下:

    Windows中编译和链接机制

    8.7 可执行文件运行时的必要条件

    EXE文件中函数和变量的内存地址是如何来表示的呢?
    • 在EXE文件的开头给函数和变量分配了虚拟内存地址,程序运行时,虚拟内存地址会转换为实际内存地址;
    • 链接器会在开头,追加转换内存地址所必需的信息,这个信息称为再配置信息;
    • 在配置信息,就成了函数和变量的相对地址;
    • 在源代码中,函数和变量是分散记数的,在链接后的EXE中,函数和变量就会变成连续排列的组,这样就可以用相对于起始地址的偏移量来表示函数和变量。
    链接后EXE的构造

    8.8 程序加载时会生成堆和栈

    • 程序加载到内存后,会生成栈和堆;
    • 栈存储局部变量,堆用来存储程序运行时的任意数据及对象的内存区域;
    • 栈中对数据进行存储和舍弃的代码,由编译器自动生成,而堆的内存空间则需要程序员明确申请分配或者释放,在高级语言中,编译器会自动生成指定栈和堆大小的代码;
    加载到内存中的程序由4部分组成

    8.9 有点难度的Q&A



    问题答案:

    答案

    第9章 操作系统和应用的关系

    本章问题:

    问题

    本章重点


    9.1 操作系统功能的历史

    • 操作系统的前身是监控程序,作用是加载和运行程序;
    • 后来人们发现很多程序都有共同的部分,例如用键盘输入、用显示屏输出等,于是把这些程序也加入了监控程序当中;
    • 于是,更多的有用程序被加入了监控程序中,渐渐变身成为了操作系统..........
    监控程序是操作系统的雏形
    初期的操作系统 = 监控系统 + 输入输出程序
    操作系统是多个程序的集合体

    9.2 要意识到操作系统的存在

    应用程序通过操作系统间接向硬件发送指令。
    比如printf()、time()函数运行的结果,是面向操作系统而非硬件的,操作系统接到指令后,首先解释这些指令,然后会对时钟IC和显示器的IO进行控制。

    应用程序通过操作系统间接控制硬件

    9.3 系统调用和高级编程语言的移植性

    • 操作系统的硬件控制功能,通常是通过一些小的函数集合体的形式提供的,这些函数及调用这些函数的行为统称为系统调用;
    • C语言等高级语言一般不依赖于操作系统,因为认为希望不会因为操作系统的不同而重写大量的代码;所以高级语言使用独立的函数名,然后在编译时转换成相应的系统调用;
    • 高级语言也可以直接进行系统调用,不过会影响可移植性;
    高级编程语言的函数调用在编译后变成了系统调用

    9.4 操作系统和高级编程语言使硬件抽象化

    操作系统的系统调用,给程序的编写带来的巨大的方便。


    9.5 Windows操作系统的特征

    • 32位操作系统(书有点过时了):在过去的16位操作系统中,处理32位的数据类型相当于处理两次16位数据类型,要花费更多的时间;有了32位
      系统,处理一次就够了,因此使用32位数据类型不会降低运行速度;
    • 通过API函数集实现系统调用:API通过多个DLL文件来提供;
    • 提供采用了GUI的用户界面:编写GUI较为困难,因为操作流程是由用户决定而非程序员决定,因此要考虑所有可能的情况;
    • 通过WYSIWYG来打印输出;
    • 提供多任务功能;
    • 提供网络功能及数据库功能;
    • 通过即插即用实现设备驱动的自动设定;

    问题答案: 问题答案


    第10章 通过汇编语言了解程序的实际构成

    本章问题:

    问题

    本章重点:

    当然是汇编。


    10.1 汇编语言和本地代码是一一对应的

    1. 使用汇编语言有助于理解本地代码。直接打开本地代码只能看到数值的罗列,通过添加助记符,如加法运算add,比较运算cmp等,可以更好地理解本地代码。使用助记符的语言被称为汇编语言,通过查看汇编语言的源代码,可以更容易地理解本地代码。
    2. 汇编语言和本地代码是一一对应的,所以可以通过本地代码反汇编得到汇编代码。但是高级语言和本地代码不是一一对应的,所以反编译到高级语言比较困难,而且完全还原是不太可能的。

    10.2 通过编译器输出汇编语言的源代码

    原书作者通过编译命令把源代码输出为汇编代码,汇编文件的后缀为.asm(assemble)。

    源代码
    汇编

    10.3 不会转换成本地代码的伪指令

    汇编语言的源代码,是由转换成本地代码的指令(操作码)和针对汇编器的伪指令组成的。
    伪指令负责把程序的构造及汇编的方法指示给汇编器,但是伪指令本身是无法转换成本地代码的。

    伪指令
    其中由伪指令segmentends围起来的部分,是程序中命令和数据的集合体,称之为段定义。_TEXT_DATA_BSS是段定义的名称。
    group表示把_BSS_DATA这两个段定义汇总名为DGROUP的组。
    _AddNum proc_AddNum endp围起来的部分,是函数AddNum的范围,同理_MyFunc procMyFunc endp 围起来的部分表示函数MyFunc的范围。这两个函数都置于_TEXT中,表示属于_TEXT段定义。虽然源代码中的指令和数据比较混乱,但是通过段定义,汇编之后会转换成划分整齐的本地代码。
    end表示源代码的结束。

    10.4 汇编语言的意思是“操作码”+“操作数”

    操作码是指令动作,操作数是指令对象。

    常用操作码
    主要寄存器

    10.5 最常用的mov指令

    mov指令中有两个操作数,分别指定数据的存储地和来源。
    如果操作数没有用[]围起来,就表示对其值进行操作,否则的话会把值解释为内存地址,然后对相应地址中的值进行操作。

    两种情况

    10.6 对栈进行push和pop

    栈是存储临时数据的区域,数据的读取要符合先进后出原则。

    栈模型

    10.7 函数调用机制

    函数调用需要依赖栈的作用。
    下图为在MyFunc函数中调用AddNum函数的处理内容:


    函数调用
    • (1)、(2)、(7)、(8)的处理适用于C语言中所有函数。
    • (3)和(4)表示传递给AddNum的函数通过push入栈。
    • (5)的call指令把程序流程跳转到了操作数中指定的AddNum函数所在的内存地址,AddNum处理完毕后,程序流程会必须回到(6)这一行。可以在调用AddNum之前把(6)指令的内存地址入栈,调用完毕后ret指令再把(6)指令的内存地址pop出来,从而使程序流程回到(6);
    • (6)是把栈指针向高位移动两位,实际上就是使123和456出栈;
    • 源代码中有个变量c指向AddNum(123,456)的运算结果,但是c变量在之后并没有用到,所以编译器就会自动优化,没有生成与之相关的汇编代码。

    10.8 函数内部的处理

    下图为call AddNum后AddNum函数内部的处理过程。


    函数内部的处理
    • ebp在(1)、(5)中入栈、出栈,是为了把ebp的值还原到函数调用前的状态,因为ebp之前可能在其它地方被使用;
    • 指令(2)中把栈指针寄存器esp的值赋给ebp,这是因为在mov指令中方括号[]中的参数不允许使用esp,所以要用ebp来代替;
    • 使用栈中的数据是通过方括号中的ebp + 相应字节数来实现的,比如(3)中通过[ebp + 8]指定栈中的123,并用mov存储在eax中;
    • 指令(4)中的add指令把123和456相加存储在eax中;
    • 函数的参数通过栈来传递,返回值是通过寄存器来返回;
    • 指令(6)的ret运行后,函数返回目的地的内存地址会自动出栈,程序流程就会跳至函数调用的下一行;
    • 可按照图10-4、10-5中a、b、c、d、e、f的顺序来看函数调用时栈的状态变化。


    10.9 始终确保全局变量用的内存空间

    简单地说,在Borland C++中,初始化和非初始化的全局变量分别被划到两个不同的段定义中,未被初始化的变量都会被设定为0进行初始化。


    10.10 临时确保局部变量用的内存空间

    临时变量储存在寄存器和栈中。
    由于寄存器的访问速度较快,寄存器空闲时就使用寄存器来存储局部变量,否则就用栈。
    函数调用完毕后,栈中局部变量的值就会被销毁(通过恢复栈指针的方式)。


    10.11循环处理的实现方法

    循环源代码:


    循环源代码
    循环汇编代码
    • 对ebx执行xor异或运算,使ebx清零,这比mov ebx 0 要快,ebx清零其实就等价于i=0;
    • ebx初始化后调用MySub,然后返回,进行第三行指令,把ebx加一;
    • cmp是把ebx的值与10比较,结果存储在标志寄存器中
    • jl 是jump on less than ,如果前面的比较指令的值是“小”的话,就跳转至@4处的指令,从而实现了循环。

    10.12 条件分支的实现方法

    与循环类似。
    条件分支C++源代码:



    条件分支汇编代码:



    10.13 了解程序运行方式的必要性

    了解程序运行方式有助于我们更好的理解程序出错的原因。
    下面的代码是两个函数更新同一个全局变量:


    实际的汇编代码:

    可以看出,counter的值乘2是把counter的值读入累加寄存器后实现的。MyFunc1和MyFunc2都是把counter的值乘2,最后理应是4倍,但是MyFunc1运行时如果尚未来的及把eax中两倍的数值写入到counter中去MyFunc2就读取了counter的值,最后运算的结果是counter的值只变为了原来的两倍。

    因此为了避免这种错误,我们可以采用以函数或者C语言代码的行为单位来禁止线程切换的锁定方法。

    问题答案: 答案

    第11章 硬件控制方法

    本章提问:

    问题

    11.1 应用和硬件无关?

    Windows应用通过调用Windows操作系统的API(系统调用)来间接控制硬件。


    应用通过操作系统间接控制硬件

    下面是一个🌰,调用WindowsAPI中的TextOut函数在窗口中显示字符串。



    11.2 支持硬件输入输出的IN指令和OUT指令

    IN指令是把指定端口号的端口数据存储在CPU内的寄存器中,OUT指令是把CPU寄存器中的数据输出到指定端口号的端口当中。
    每个硬件都会有各自的I/O控制器,一个控制器可以控制多个端口,端口就是内存,储存着要交换的数据,端口用端口号来识别。


    11.3 编写测试用的输入输出程序

    这个例子是通过编写程序控制计算机内部的蜂鸣器发声。
    小知识:C代码可以和汇编代码混写,但是汇编代码必须写在asm{}的大括号里。
    在AT兼容机中,蜂鸣器的端口号是61H(末尾的H表示的是16进制数的意思),通过把向蜂鸣器端口发送的数据的后两位设为1或0,来控制蜂鸣器发声、关闭。
    实现方法:

    1. 与0进行OR运算,不改变原二进制序列;与1进行AND运算,不改变原二进制序列。
    2. 因此,可以让数据与03H(二进制为00000011)进行OR运算,这样数据前6位不变,后两位变为1,进而控制蜂鸣器发声;同样,与FCH(11111100)进行AND运算,前6位不变,后两位为0,进而控制蜂鸣器关闭。
      代码如下:

      通过IN和OUT指令,在寄存器和61H端口中输入输出数据,控制蜂鸣器发声。
      程序在低版本Windows中可以运行,高版本Windows禁止了应用直接控制硬件,这个程序会被禁止运行。

    11.4 外围设备的中断请求

    每个设备会有自己的中端编号。
    收到中断请求后,CPU会把当前任务暂时挂起来处理中断请求。
    计算机通过中断控制器来管理中断请求。


    中断请求的顺序
    中断控制器的功能

    11.5 用中断来实现实时处理

    如题。


    11.6 DMA可实现短时间内传输大量数据

    DMA,Direct Memory Access,指不通过CPU的情况下,外围设备直接和主内存进行数据传送,这样会更快,DMA不是必选项,也有自己动编号。



    11.7文字及图片的显示机制

    显示器中显示的内容储存在VRAM(Video RSM)中,在程序中,向VRAM写入数据,就会在显示器中显示出来,实现该功能的程序,由BIOS(Basic Input Output System)提供,并借助中断运行。
    现代计算机中,显卡等专用硬件一般都配置有与主内存相独立的VRAM和GPU(Graphics Processing Unit 图形处理器),过去的VRAM是主内存的一部分。



    问题答案:

    答案

    第12章 让计算机“思考”




    完。

    相关文章

      网友评论

          本文标题:《程序是怎样跑起来的》(下)

          本文链接:https://www.haomeiwen.com/subject/cvrcoqtx.html