计算机是如何跑起来的百度云,《程序是怎样跑起来的》(下)

学习笔记

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

本章问题:

4e202e4f35f0

问题

本章重点:

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

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

4e202e4f35f0

一个例子1

4e202e4f35f0

一个例子2

图中栗子的源代码文件命名为Sample1.c。

4e202e4f35f0

源代码需要转换成本地代码才能运行

8.2 本地代码的内容

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

4e202e4f35f0

记事本打开本地代码

把本地代码Dump一下,每一个字节用2位16进制数(每个16进制数代表4位二进制数,2位16进制数恰好代表8位即1字节)来表示:

4e202e4f35f0

本地代码的真是面目是数值的罗列

8.3 编译器负责转换源代码

不同的语言有各自的编译器;

不同类的CPU有不同的机器语言,需要不同的编译器;

编译器也是应用程序,也需要运行环境;

存在交叉编译器,在某一环境下运行,可以生成另一环境下的本地代码。

4e202e4f35f0

通过命令行编译上面的栗子

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

编译生成的是.obj文件(目标文件),而不是.exe文件,无法直接运行;

如果源代码中引用了其它的函数(如上面例子中的sprintf()、MessageBox()),就需要把储存着这些函数的目标文件与此目标文件相结合;

完成此工作的是链接器,最后生成.exe文件

4e202e4f35f0

链接上面的栗子

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中。

用下图总结下:

4e202e4f35f0

Windows中编译和链接机制

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

EXE文件中函数和变量的内存地址是如何来表示的呢?

在EXE文件的开头给函数和变量分配了虚拟内存地址,程序运行时,虚拟内存地址会转换为实际内存地址;

链接器会在开头,追加转换内存地址所必需的信息,这个信息称为再配置信息;

在配置信息,就成了函数和变量的相对地址;

在源代码中,函数和变量是分散记数的,在链接后的EXE中,函数和变量就会变成连续排列的组,这样就可以用相对于起始地址的偏移量来表示函数和变量。

4e202e4f35f0

链接后EXE的构造

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

程序加载到内存后,会生成栈和堆;

栈存储局部变量,堆用来存储程序运行时的任意数据及对象的内存区域;

栈中对数据进行存储和舍弃的代码,由编译器自动生成,而堆的内存空间则需要程序员明确申请分配或者释放,在高级语言中,编译器会自动生成指定栈和堆大小的代码;

4e202e4f35f0

加载到内存中的程序由4部分组成

8.9 有点难度的Q&A

4e202e4f35f0

4e202e4f35f0

问题答案:

4e202e4f35f0

答案

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

本章问题:

4e202e4f35f0

问题

本章重点

9.1 操作系统功能的历史

操作系统的前身是监控程序,作用是加载和运行程序;

后来人们发现很多程序都有共同的部分,例如用键盘输入、用显示屏输出等,于是把这些程序也加入了监控程序当中;

于是,更多的有用程序被加入了监控程序中,渐渐变身成为了操作系统..........

4e202e4f35f0

监控程序是操作系统的雏形

4e202e4f35f0

初期的操作系统 = 监控系统 + 输入输出程序

4e202e4f35f0

操作系统是多个程序的集合体

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

应用程序通过操作系统间接向硬件发送指令。

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

4e202e4f35f0

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

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

操作系统的硬件控制功能,通常是通过一些小的函数集合体的形式提供的,这些函数及调用这些函数的行为统称为系统调用;

C语言等高级语言一般不依赖于操作系统,因为认为希望不会因为操作系统的不同而重写大量的代码;所以高级语言使用独立的函数名,然后在编译时转换成相应的系统调用;

高级语言也可以直接进行系统调用,不过会影响可移植性;

4e202e4f35f0

高级编程语言的函数调用在编译后变成了系统调用

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

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

9.5 Windows操作系统的特征

32位操作系统(书有点过时了):在过去的16位操作系统中,处理32位的数据类型相当于处理两次16位数据类型,要花费更多的时间;有了32位

系统,处理一次就够了,因此使用32位数据类型不会降低运行速度;

通过API函数集实现系统调用:API通过多个DLL文件来提供;

提供采用了GUI的用户界面:编写GUI较为困难,因为操作流程是由用户决定而非程序员决定,因此要考虑所有可能的情况;

通过WYSIWYG来打印输出;

提供多任务功能;

提供网络功能及数据库功能;

通过即插即用实现设备驱动的自动设定;

问题答案:

4e202e4f35f0

问题答案

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

本章问题:

4e202e4f35f0

问题

本章重点:

当然是汇编。

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

使用汇编语言有助于理解本地代码。直接打开本地代码只能看到数值的罗列,通过添加助记符,如加法运算add,比较运算cmp等,可以更好地理解本地代码。使用助记符的语言被称为汇编语言,通过查看汇编语言的源代码,可以更容易地理解本地代码。

汇编语言和本地代码是一一对应的,所以可以通过本地代码反汇编得到汇编代码。但是高级语言和本地代码不是一一对应的,所以反编译到高级语言比较困难,而且完全还原是不太可能的。

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

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

4e202e4f35f0

源代码

4e202e4f35f0

汇编

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

汇编语言的源代码,是由转换成本地代码的指令(操作码)和针对汇编器的伪指令组成的。

伪指令负责把程序的构造及汇编的方法指示给汇编器,但是伪指令本身是无法转换成本地代码的。

4e202e4f35f0

伪指令

其中由伪指令segment和ends围起来的部分,是程序中命令和数据的集合体,称之为段定义。_TEXT、_DATA、_BSS是段定义的名称。

group表示把_BSS和_DATA这两个段定义汇总名为DGROUP的组。

_AddNum proc 和 _AddNum endp围起来的部分,是函数AddNum的范围,同理_MyFunc proc和MyFunc endp 围起来的部分表示函数MyFunc的范围。这两个函数都置于_TEXT中,表示属于_TEXT段定义。虽然源代码中的指令和数据比较混乱,但是通过段定义,汇编之后会转换成划分整齐的本地代码。

end表示源代码的结束。

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

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

4e202e4f35f0

常用操作码

4e202e4f35f0

主要寄存器

10.5 最常用的mov指令

mov指令中有两个操作数,分别指定数据的存储地和来源。

如果操作数没有用[]围起来,就表示对其值进行操作,否则的话会把值解释为内存地址,然后对相应地址中的值进行操作。

4e202e4f35f0

两种情况

10.6 对栈进行push和pop

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

4e202e4f35f0

栈模型

10.7 函数调用机制

函数调用需要依赖栈的作用。

下图为在MyFunc函数中调用AddNum函数的处理内容:

4e202e4f35f0

函数调用

(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变量在之后并没有用到,所以编译器就会自动优化,没有生成与之相关的汇编代码。

4e202e4f35f0

10.8 函数内部的处理

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

4e202e4f35f0

函数内部的处理

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的顺序来看函数调用时栈的状态变化。

4e202e4f35f0

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

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

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

临时变量储存在寄存器和栈中。

由于寄存器的访问速度较快,寄存器空闲时就使用寄存器来存储局部变量,否则就用栈。

函数调用完毕后,栈中局部变量的值就会被销毁(通过恢复栈指针的方式)。

10.11循环处理的实现方法

循环源代码:

4e202e4f35f0

循环源代码

4e202e4f35f0

循环汇编代码

对ebx执行xor异或运算,使ebx清零,这比mov ebx 0 要快,ebx清零其实就等价于i=0;

ebx初始化后调用MySub,然后返回,进行第三行指令,把ebx加一;

cmp是把ebx的值与10比较,结果存储在标志寄存器中

jl 是jump on less than ,如果前面的比较指令的值是“小”的话,就跳转至@4处的指令,从而实现了循环。

10.12 条件分支的实现方法

与循环类似。

条件分支C++源代码:

4e202e4f35f0

条件分支汇编代码:

4e202e4f35f0

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

了解程序运行方式有助于我们更好的理解程序出错的原因。

下面的代码是两个函数更新同一个全局变量:

4e202e4f35f0

实际的汇编代码:

4e202e4f35f0

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

4e202e4f35f0

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

问题答案:

4e202e4f35f0

答案

第11章 硬件控制方法

本章提问:

4e202e4f35f0

问题

11.1 应用和硬件无关?

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

4e202e4f35f0

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

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

4e202e4f35f0

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

IN指令是把指定端口号的端口数据存储在CPU内的寄存器中,OUT指令是把CPU寄存器中的数据输出到指定端口号的端口当中。

每个硬件都会有各自的I/O控制器,一个控制器可以控制多个端口,端口就是内存,储存着要交换的数据,端口用端口号来识别。

4e202e4f35f0

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

这个例子是通过编写程序控制计算机内部的蜂鸣器发声。

小知识:C代码可以和汇编代码混写,但是汇编代码必须写在asm{}的大括号里。

在AT兼容机中,蜂鸣器的端口号是61H(末尾的H表示的是16进制数的意思),通过把向蜂鸣器端口发送的数据的后两位设为1或0,来控制蜂鸣器发声、关闭。

实现方法:

与0进行OR运算,不改变原二进制序列;与1进行AND运算,不改变原二进制序列。

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

代码如下:

4e202e4f35f0

通过IN和OUT指令,在寄存器和61H端口中输入输出数据,控制蜂鸣器发声。

程序在低版本Windows中可以运行,高版本Windows禁止了应用直接控制硬件,这个程序会被禁止运行。

11.4 外围设备的中断请求

每个设备会有自己的中端编号。

收到中断请求后,CPU会把当前任务暂时挂起来处理中断请求。

计算机通过中断控制器来管理中断请求。

4e202e4f35f0

中断请求的顺序

4e202e4f35f0

中断控制器的功能

11.5 用中断来实现实时处理

如题。

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

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

4e202e4f35f0

11.7文字及图片的显示机制

显示器中显示的内容储存在VRAM(Video RSM)中,在程序中,向VRAM写入数据,就会在显示器中显示出来,实现该功能的程序,由BIOS(Basic Input Output System)提供,并借助中断运行。

现代计算机中,显卡等专用硬件一般都配置有与主内存相独立的VRAM和GPU(Graphics Processing Unit 图形处理器),过去的VRAM是主内存的一部分。

4e202e4f35f0

问题答案:

4e202e4f35f0

答案

第12章 让计算机“思考”

完。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值