程序是如何跑起来的 笔记

第一章:CPU是什么
 
CPU内部由寄存器、控制器、运算器和时钟四个部分构成。
寄存器可用来暂存指令、数据等处理对象,可以将其看作是内存的一种。根据种类的不同,一个CPU内部会有20~100个寄存器。
控制器负责把内存上的指令、数据等读入寄存器,并根据指令的执行结果来控制整个计算机。
运算器负责运算从内存读入寄存器的数据。
时钟负责发出CPU开始计时的时钟信号。
DRAM Dynamic Random Access Memory
 
主要了解寄存器
程序是把寄存器作为对象来描述的。
汇编语言 Assembly
汇编器 assembler 把汇编语言转化成为机器语言的程序
汇编与反汇编的理解
机器语言和汇编语言的理解
mov add 分别是数据存储和相加
 
程序示例:
mov eax, dword ptr [ebp-8]                   把数值从内存复制到eax中
add eax, dword ptr [ebp-0Ch]                eax的数值和内存的数值相加
mov dword ptr [ebp-4], eax                   把eax的数值存储在内存中
 
 
eax和ebp分别是累加寄存器和基址寄存器
寄存器大致分为8类:
 
内存中配置的程序示例:
 
程序的流程分为顺序执行、条件分支和循环三种。
显示绝对值的流程:
 
条件分支和循环中使用的是跳转指令,会参照当前执行的运算结果来判断是否跳转。
标志寄存器,无论当前累加寄存器的运算结果是负数、零还是正数,标志寄存器都会将其保存。
 
函数的调用机制;
call指令会把调用函数后要执行的指令地址存储在栈中。
return命令的功能是把保存在栈中的地址设定到程序计数器中。
 
 
 
基址寄存器和变址寄存器的使用:
 
机器语言指令的主要类别和功能:
 

第二章:数据用二进制如何表达
 
几个问题:
位和字节的关系
二进制和十进制的转换
左右移,逻辑以及算术左右移
补码是啥?
 
8位二进制的数被称为一个字节
位权的理解
基数的理解
移位操作,逻辑左右移和算术左右移的区别:

先取反然后加一
符号扩充:
就是指在保持值不变的前提下将其转换成16位和32位的二进制数。
最高位为0的情况下直接补上0,最高位为1的情况下直接补1。

逻辑运算:
! & | ^
第三章:小数运算
小问题:
二进制的0.1,用十进制表示是多少?
十进制的0.625用二进制如何表达?
你了解浮点数吗?
二进制的基数是多少?   2
通过把0作为数值范围的中间值,从而在不使用符号位的情况下来表示负数的表示方法称为什么?
二进制小数转十六进制小数
 
小数是如何计算错误的:
十进制的小数有些是无法转换成为二进制数的。
二进制小数的表达方式

浮点数
双精度浮点数类型用64位,单精度浮点数类型用32位来表示全体小数。
double float
 
浮点数是指用符号、尾数、基数和指数这四部分来表示的小数。
计算机内部使用二进制,所以基数是2。实际的数据中往往不考虑基数,只用符号、尾数、指数这三个部分即可表示浮点数。

内部构造:

符号部分,1代表负数,0代表正数。
尾数部分用的是“将小数点前面的值固定为1的正则表达式”
而指数部分用的则是“EXCESS系统表现”。
主要是为了表示负数不使用符号位。
8位的话,最大值是255,用127表示0,指数部分表示范围的中间值设为0。即-127到128

指数部分是01111110是十进制的126,用EXCESS系统表现就是-1,(126-127)。
根据正则表达式的规则,小数点前面的第一位是1,因此尾数部分100000000000000000000000实际上表示的是
1.10000000000000000000000这个二进制数。将尾数部分的二进制数转换成十进制数,结果就是(1*2的0次幂)+(1*2的-1次幂)=1.5。
因此0-01111110-10000000000000000000000这个单精度浮点数,表示的就是“+1.5*2的-1次幂”2的-1次幂是0.5,+1.5*0.5=+0.75。
 
第四章  使用内存

数据信号引脚有D0~D7共八个,表示一次可以输入输出8位(=1个字节)的数据。
此外,地址信号引脚有A0~A9共十个,表示可以指定0000000000~1111111111共1024个地址。
因此这个内存IC可以存储1024个1字节的数据。所以该内存IC的容量就是1KB。
 
 
如何读写数据:

数组的理解
栈和队列的理解
环状缓存区
链表的理解
二叉树的理解
 
 
第五章  内存和磁盘
 
虚拟内存的理解
 
扇区、簇的理解
 
 
第六章 尝试压缩数据
 
几个小问题:
文件存储的基本单位是什么?
DOC、LZH和TXT那个是压缩文件的扩展名称?
RLE、哈夫曼算法?
bmp格式?
可逆压缩与非可逆压缩?
 
1. 文件是字节数据的集合体
2. LZH是LHA等工具压缩过的文件的扩展名。
3. AAABB这个数据压缩后就是A3B2。
4. 半角用一个字节表示,汉字用两个字节表示。
5. BMP是原始格式。
6. JPEG是非可逆压缩。
 
 
RLE算法机制:
AAAAAABBCDDEEEEEF可以用A6B2C1D2E5F1来表示,即17->12。压缩成功。
把文件内容用“数据*重复次数”的形式来表示的压缩方法称为RLE(Run Length Encoding)。
该算法简单,适合图像等数据经常连续出现的文件。
 
哈夫曼算法:
哈夫曼于1952年提出的算法。
 
摩尔斯编码:
1837年摩尔斯提出。
短点和长点分别表示内容。
 
用二叉树来实现哈夫曼编码
 
以AAAAAABBCDDEEEEEF为例。

编码完成后,比如10001,即BE。
 
可逆压缩与非可逆压缩:
Windows的标准图像数据形式是BMP,bit可以直接映射(mapping),因此便有了BMP=bitmap这一个名称。
JPEG GIF是非可逆压缩,即无法还原至原压缩内容。
 
 
第七章 程序是在何种环境中运行的
跳过
 
第八章 从源文件到可执行文件
跳过
第九章 操作系统和应用的关系
初期的操作系统=监控程序+基本的输入和输出程序
第十章 通过汇编语言了解程序的实际构成

本地语言到C语言的反编译比反汇编要难的多,C语言的源代码同本地代码不是一一对应的。
 
编译器输出汇编语言的源代码

编译器生成的汇编语言:

汇编语言的源代码,是由转换成本地代码的指令(后面讲述的操作码)和针对汇编器的伪指令构成的。
伪指令负责把程序的构造及汇编的方法指示给汇编器(转换程序)。不过伪指令本身是无法汇编转换成本地代码的。
摘出部分:

由伪指令segment和ends围起来的部分,是给构成程序的命令和数据的集合体加上一个名字而得到的,称为段定义。
源代码的开始位置,定义了3个名称分别为_TEXT、_DATA、_BSS的段定义。
_TEXT是指令的段定义。
_DATA是被初始化的数据的段定义。
_BSS是尚未初始化的数据的段定义。
这样的配置顺序就成了_TEXT、_DATA、_BSS,这样也确保了内存的连续性。group这一伪指令,表示的是把_BSS和_DATA这两个段定义汇总为名为DGROUP的组。
_AddNum和_MyFun的_TEXT segment和_TEXT ends,表示_AddNum和_MyFun是属于_TEXT这一段定义的。
 
_AddNum proc和_AddNum endp围起来的部分,以及_MyFun proc和_MyFun endp围起来的部分,分别表示AddNum函数和MyFunc函数的范围。
伪指令proc和endp围起来的部分,表示的是过程的范围。在汇编语言中,这种相当于C语言的函数的形式称为过程。
末尾的end伪指令,表示的是源代码的结束。
汇编语言的语法是“操作码+操作数”
操作码表示的是指令动作,操作数表示的是指令对象。
操作码和操作数罗列在一起的语法,就是一个英文的指令文本。
操作码是动词,操作数相当于宾语。

本地代码加载到内存后才能运行。内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把指令和数据读出,然后再将其存储在CPU内部的的寄存器中进行处理。

寄存器的名称会通过汇编语言的源代码指定给操作数。
内存中的存储区域是用地址编号来区分的。
此为,CPU内部也有程序员无法直接操作的寄存器。比如表示运算结果正负及溢出状态的标志寄存器及操作系统专用的寄存器等,都无法通过程序员编写程序直接进行操作。
 
最常用的MOV指令:
mov指令的两个操作数,分别用来指定数据的存储地和读出源。
操作数中可以指定寄存器、常数、标签(附近在地址前),以及方括号([])围起来的内容。
如果制定了没有用方括号围起来的内容,就表示对该值进行处理;如果指定了用方括号围起来的内容,方括号中的值则会被解释为内存地址,然后对该内存地址对应的值进行读写操作。
mov ebp,esp    esp寄存器中的值被直接存储在ebp寄存器中
mov eax, dword ptr [ebp+8]    ebp寄存器的值加8后得到的值会被解释为内存地址。dword ptr(double word pointer)表示从指定内存地址读出4字节的数据。
 
 
对栈进行push和pop
数据存储时是从内存的大地址到小地址,即自上而下。

不喜欢这个图,自上而下比较好理解。
push和pop指令中只有一个操作数。这是因为,对栈进行读写的内存地址是由esp寄存器(栈指针)进行管理的。
push和pop指令运行后,esp寄存器的值会自动进行更新(push指令是-4,pop指令是+4),因而程序员就没有必要指定内存地址了。
 
push指令运行后,操作数中指定的值就会被自动push入栈,pop指令运行后,最后储存在栈中的值就会被pop到指定的操作数中出栈。
 
栈 LIFO(Last In First Out)
 
函数调用机制;

(1)、(2)、(7)、(8)适用于C语言中所有的函数。
(3)、(4)表示的是将传递给AddNum函数的参数通过push入栈。
(5)的call指令,把程序流程跳转到了操作数中指定的AddNum函数所在的内存地址处。
在汇编语言中,函数名表示的是函数所在的内存地址。
AddNum函数处理完毕后,程序流程必须要返回到编号(6)这一行。第六行有错误,应该是add esp,8。
call指令运行后,call指令的下一行((6)这一行)的内存地址(调用函数完毕后要返回的内存地址)会自动
地push入栈。该值会在AddNum函数处理的最后通过ret指令pop出栈,然后程序就会返回到(6)这一行。
第七行处有错误。应该是读出栈中的数值存入esp寄存器。
 
(6)部分会把栈中存储的两个参数进行销毁处理。也就是第5章提到的栈清理处理。
虽然通过使用两次pop指令也可以实现,不过采用esp寄存器加8的方式会更有效率(处理一次即可)。
虽然内存中的数据实际上还残留着,但只要把esp寄存器的值更新为数据存储地址前面的数据位置,该数据也就相当于被销毁了。

函数的内部处理:

ebp寄存器的值在(1)中入栈,在(5)中出栈。函数中会用到ebp寄存器,所以将ebp中的值先入栈保存,等待后续恢复。
(2)中把负责管理栈地址的esp寄存器的值赋值到了ebp寄存器中。这是因为,在mov指令中方括号内的参数,是不允许指定esp
寄存器的。因此采用了不直接通过esp,而是用ebp寄存器来读写栈内容的方法。
(3)是用[ebp+8]指定栈中存储的第一个参数123,并将其读出到eax寄存器中。像这样,不使用pop指令,也可以参照栈的内容。
而选择eax,是因为eax寄存器是负责运算的累加寄存器。
通过(4)的add指令,把当前的eax寄存器的值同第二个参数相加后的结果存储在eax寄存器中。[ebp+12]是用来指定第2个参数456的。在C语言中,函数的返回值必须通过eax寄存器返回,这也是规定。不过,和ebp寄存器不同的是,eax寄存器的值不用还原到
原始状态。
希望了解函数的参数是通过寄存器和栈来传递,返回值是通过寄存器来返回的。
(6)中ret指令运行后,函数返回目的的内存地址会自动出栈,据此,程序流程就会跳转返回到代码清单10-4(6),即(call _AddNum的
下一行)。栈的最高位的数据地址,是一直存储在esp寄存器中的。

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

汇编代码如下:

初始化的全局变量,会被汇总到名为_DATA的段定义中,没有初始化的全局变量,会汇总到名为_BSS的段定义中。
指令被汇总到名为_TEXT的段定义中。
其中_DATA segment和_DATA ends、_BSS segment和_BSS ends、_TEXT segment和_TEXT ends,这些都是表示各段定义范围的伪指令。
 
_DATA段定义的内容:
(4)中的_a1 label dword定义了_a1这个标签。
标签表示的是相对于段定义起始位置的位置,所以相对位置是0。
_a1就相当于全局变量a1。编译后的函数名和变量名前会附加一个下划线(_),这就是编译器(Borland C++)的规定。
(5)中的dd 1值的是,申请分配了4字节的内存空间,存储着1这个初始值。dd(define double word)表示的是有两个长度为2的字节领域(word),也就是4字节的意思。
也就是int a1=1; 变换成了_a1 label dword和dd 1。同样的处理见a2~a5。
 
 
_BSS段定义的内容:
定义了全局变量b1~b5的标签_b1~_b5。
(6)的db 4 dup(?)表示的是申请分配了4字节的领域,但值尚未确定(这里用?来表示)的意思。
db(define byte)表示有1个长度是1字节的内存空间。db 4 dup(?)表示的是4个长度是1字节的内存空间。
而dd 4表示的是双字节(=4字节)的内存空间中存储的值是4。
 
临时确保局部变量用的内存空间
 
为什么局部变量只能在定义该变量的函数内见参阅呢?
局部变量时临时保存在寄存器和栈中。
寄存器空闲时就使用寄存器,寄存器空间不足的话就使用栈。
(7)表示的是MyFunc函数的范围。
(8)表示的是往寄存器中分配局部变量的部分。
仅仅对局部变量进行定义是不够的,只有在给局部变量赋值时,才会被分配到寄存器的内存区域。c1~c5。
(9)剩余的5个局部变量c6~c10就被分配了栈的内存空间。
函数入口(10)处的add esp,-20指的是,对栈数据存储位置的esp寄存器(栈指针)的值做减20的处理。
为了确保内部变量c6~c10在栈中,就需要保留5个int类型的局部变量(4*5=20)所需要的空间。
(11)中的mov ebp,esp这一处理,指的是把当前esp寄存器的值复制到ebp寄存器中。然后通过(12)这一move esp,ebp的处理
把esp寄存器的值还原到原始状态,从而对申请分配的栈空间进行释放,这时栈中用到的局部变量就消失了。
这也是栈的清理处理。

(9)往栈空间中代入数值的部分。借助mov ebp,esp这个处理,esp寄存器的值被保存到了ebp寄存器中,通过使用[ebp-4]…
mov dword ptr [ebp - 4], 6表示的就是,从申请分配的内存空间的下端(ebp寄存器指示的位置)开始往前4字节的地址([ebp - 4])
中,存储着6这一4字节的数据。
 
 
循环处理的实现方法:

汇编语言的源代码中,循环是通过比较指令(cmp)和跳转指令(jl)来实现的。
MyFunc函数中用到的局部变量只有i,变量i申请分配了ebx寄存器的内存空间。for语句的括号中的i=0;被转换
成了xor ebx,ebx这一处理。
xor指令会对左起第一个操作数和右起第二个操作数进行XOR运算,然后把结果存储在第一个操作数中。
由于这里把第一个操作数和第二个操作数指定为了ebx,因此就变成了对相同数值进行XOR运算。相较于mov指令,
xor指令的速度更快。
 
ebx寄存器的值初始化后,会通过call指令调用MySub函数。从函数返回后,通过inc指令对ebx寄存器的值做加1处理,相当于i++。
 
下一行的cmp指令是用来对第一个操作数和第二个操作数的数值进行比较的指令。
cmp ebx,10,相当于C语言的i<10。汇编语言中比较指令的结果,会存储在CPU的标志寄存器中。
汇编语言中有多个跳转指令,这些跳转指令会根据标志寄存器的值来判定是否需要跳转。例如,最后一行的jl,jump on less than。
jl short @4的意思就是,前面运行的比较指令的结果若小的话就跳转到@4这个标签。

条件分支的实现方法

代码中用到了三种跳转指令,分别是比较结果小时跳转jle(jump on less or equal)、大时跳转jge(jump on greater or equal)、
不管结果怎样都无条件跳转的jmp。
eax寄存器表示的是变量a。
 
 
了解程序运行方式的必要性
 
11章 硬件控制方法
跳过
12章 让计算机“思考”
跳过
 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值