程序是怎样跑起来的

第一章 对程序员来说CPU是什么

  • CPU四个构成部分


    image.png
  • 寄存器种类


    image.png
  • 程序计数器 PC
    保存下一条要执行的命令的内存地址

  • 标志寄存器处理条件分支(if else)和循环机制(for(;;i<100))


    image.png
  • 栈寄存器实现函数调用机制


    image.png
  • 基址寄存器和变址寄存器实现数组功能


    image.png
  • 机器语言指令种类


    image.png

第二章 数据是用二进制表示的

  • IC的所有引脚只有直流电压0V或5V两个状态,这个特性决定了计算机只能使用二进制


    image.png
  • 负数的二进制表示:补数 取反加一


    image.png
  • 逻辑右移 空出来的位置补0


    image.png
  • 算数右移 空出来的位置用移位前符号位的值

  • 逻辑左移,算数左移都是补零

  • 逻辑运算


    image.png

计算机进行小数运算时出错的原因

  • 将0.1累加100次的结果不是10,因为十进制的0.1在二进制中是无限循环小数0.0001100110011001100(无数个1100),类似十进制的1/3=0.3333333


    image.png
  • 什么是单精度浮点数 float
    由下面三部分组成,占位32位

    | 符号位 | 指数位(8 bit) | 尾数位(23 bit) | 
    | :-:   | :-:           | :-:           |
    

    符号位(1位)

    1 : 表示负数
    0 : 表示正数
    

    指数位(8位) 使用EXCESS系统表现,值域位[-127,128]


    image.png

    尾数位(24位)
    先把小数转成二进制小数,比如11.1875的二进制小数为1011.0011


    image.png

    值域表示区间为
    正数: [1.401298 * 10^-45, 3.402823 * 10^38]
    负数: [-3.402823 * 10^38, -1.401298 * 10^-45]
    
  • 什么是双精度浮点数 double
    跟float类似,占64位

    | 符号位 | 指数位(11 bit) | 尾数位(52 bit) | 
    | :-:   | :-:           | :-:           |
    

    值域表示区间为

    正数: [4.94065645841247 * 10^-324, 1.79769313486232 * 10^308]
    负数: [-1.79769313486232 * 10^308, -4.94065645841247 * 10^-324]
    

第四章 熟练使用有棱有角的内存

  • 内存的物理机制


    image.png
  • 数组的内存模型


    image.png

第五章 内存和磁盘的亲密关系

  • 虚拟内存把磁盘作为部分内存来使用
  • 静态链接
    从目标文件中抽对应代码出来放到应用程序的目标文件,多个应用则会有多份同样的代码,浪费内存


    image.png
  • 动态链接 DLL
    同一个DLL文件的内容运行时可以被多个应用共有,内存利用率高,修改dll文件无需重新编译应用


    image.png
  • 磁盘物理结构
    Windows采用扇区方式划分磁盘,扇区是磁盘进行物理读写的最小单位,Windows对磁盘进行逻辑读写的最小单位是簇,1簇=n*扇区,也就是一次读写多个扇区增加性能


    image.png

第六章 亲自尝试压缩数据

  • RLE(Run Length Encoding 行程长度算法)
    常用于压缩传真的图像,黑白的图像,不适用于重复性不高的文本

    AAAAAABBCDDEEEEEF  17字节
    =>
    A6B2C1D2E5F1  12字节
    压缩率: 12/17=70%
    
  • 哈夫曼编码
    AAAAAABBCDDEEEEEF先按频率高到低排列的顺序整理


    image.png

    这个编码方案有问题,C(100) 和 BA(10 0)没有间隔符分不清楚,需要换一种编码方式


    image.png

    哈夫曼算法能够大幅提升压缩比率
    image.png
  • 可逆压缩和不可逆压缩
    BMP格式(bitmap)是完全未压缩的,JPEG,TIFF,GIF都会用一些技法对数据进行压缩,与前面说的RLE算法,哈夫曼算法不同的是,BMP压缩成JPEG,TIFF,GIF之后没办法还原成原来同等的质量,对图片来说是可以接受的,这叫不可逆压缩

  • 压缩算法千万种,没有最好用的压缩算法,只有最合适的压缩算法

第七章 程序是在何种环境中运行的

  • Intel的处理器是按照8086,80286,80386,80486,Pentium这样的顺序不断升级的,所以总称x86
  • 以前的写软件要针对不同的厂商不同的机器分别写一套,Windows解决了这个问题


    image.png
  • MS-DOC应用大多都是直接控制硬件的,而WINDOWS应用则是通过操作系统的系统调用完成对硬件的控制
  • 不同的操作系统,系统调用API不同
  • JAVA的跨平台是通过先编译成字节代码(byte code),运行的时候通过不同操作系统专用的JAVA虚拟机(JVM)逐行转换成本地代码运行


    image.png
  • BIOS(Basic Input/Output System)存储在ROM中,除了键盘,磁盘,显卡等基本控制程序外,还有启动引导程序的功能,开机后,BIOS确认硬件是否正常,然后启动引导程序把在硬盘的操作系统加载到内存中运行,此后就没BIOS什么事了

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

  • 计算机只能运行本地代码,还要是符合CPU指令集的本地代码


    image.png
  • 交叉编译器
    生成和运营环境的CPU不同的CPU所使用的代码,比如在ARM的计算器生成X86架构的本地代码

  • 编译生成本地文件之后,需要进行链接(link)之后才能生成可执行文件
    C语言编译生成.obj目标文件(本地代码),需要通过链接器链接生成exe文件

  • 静态链接库
    当WINDOWS系统程序调用到静态链接库的代码时,比如sprintf(),需要在cw32.lib把sprintf的代码单独抽出来,放进程序的本地代码中
    Unix系统sprintf()函数在stdio.h库里

  • DLL (Dynamic Link Library) 文件是程序运行时动态结合的文件,这种动态文件一般都由操作系统加载在内存里面
    (@todo没太搞清楚动态链接具体做了什么,后面回来补)

  • 链接机制


    image.png
  • 可执行文件内容分为再配置信息,变量组,函数组
    当可执行文件加载到内存之后会增加堆,栈两个组
    再配置信息用来设置程序起始虚拟内存地址
    栈用来实现函数调用的,栈操作由编译器生成,全程无需程序员参与
    堆用来存储程序运行时的任意数据及对象的内存,程序员可以编写程序申请分配或者释放堆内存

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

  • 操作系统发展史
    没有操作系统的年代:用机器语言编写程序,使用开关输入程序
    监控程序:先启动监控程序,按需求把各种程序一次性加载到内存中运行,后来键盘输入,显示器输出等共通的部分追加到监控程序中
    编译器:监控程序运行的程序里面出现了编译器

  • 计算机中都安装有保存日期和时间的实用时钟(Real-time clock)

  • time()printf() 都使用了系统调用,涉及到硬件基本都涉及到系统调用

  • 同样的源代码在不同的操作系统编译生成本地代码使用的系统调用不一样


    image.png
  • WYSIWYG (What You See Is What You Get) 是一种显示器看到的文本和图形可以原样打印到打印机的技术

  • WIN32 API WIN64 API 不一样,但都是通过 DLL(动态链接库)提供的。

  • 多任务功能是通过时钟分割技术实现的

  • Windows还具有以程序中的函数为单位来进行时钟分割的多线程功能

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

  • 汇编和反汇编


    image.png
  • 从本地代码反汇编成汇编语言很简单,从汇编语言反编译完全还原到原始的源代码不太可能,因为从源码到本地代码经历了各种优化,这种优化也没记录到本地代码中,与不可以压缩的原理差不多

  • 基本的操作码


    image.png
  • X86系列主要的寄存器,32位CPU开头都带了一个字母e(extended),区别于16位CPU,ax,bx,cx,dx等,64位CPU开头都带了R,RAX,RBX...


    image.png
  • 简单的MOV指令

mov ebp, esp  //esp寄存器的值存储到ebp寄存器
mov eax, dword ptr [ebp+8] //ebp+8的值被当成内存地址(指针)存到eax寄存器中
  • 32位X86系列的CPU中,进行1次PUSH/POP即可处理32位的数据
    @todo 参数是1个字节的字符 或者结构体怎么运作
  • 函数调用
    函数的参数是通过栈传递的(寄存器多了之后也有通过寄存器传值,性能好一点),返回值是通过寄存器来返回的,这里是eax寄存器保存AddNum函数的返回值
_MyFunc proc near
push    ebp         ; 保存当前函数的基址指针
mov     ebp, esp    ; 将当前栈指针赋值给基址指针

push    456         ; 将值456压入栈
push    123         ; 将值123压入栈
call    _AddNum     ; 调用_AddNum函数,跳转到下面

add     esp, 8     ; 调整栈指针,释放之前压入的参数
pop     ebp        ; 恢复之前保存的基址指针
ret                 ; 返回调用方

_MyFunc endp
_AddNum proc near
push ebp                ; 保存当前函数的基址指针
mov  ebp, esp           ; 将当前栈指针赋值给基址指针

mov  eax, dword ptr [ebp+8]    ; 将偏移为8的位置的值(第一个参数)赋给寄存器eax
add  eax, dword ptr [ebp+12]   ; 将偏移为12的位置的值(第二个参数)加到eax寄存器上

pop  ebp                ; 恢复之前保存的基址指针
ret                     ; 返回调用方

_AddNum endp
  • 全局变量
    已初始化的全局变量在_DATA段,未初始化的全局变量在_BSS段,虽然段不同,内存空间在程序初始都分配好的,所以其他函数随时可以引用全局变量
    局部变量在寄存器空闲时存在寄存器,寄存器空间不足的话就使用栈,寄存器访问速度比内存快得多多,只要寄存器有空间,编译器就会拼命压榨它
    不只是形参存在栈里,函数里面定义的局部变量也存在栈里
int a1 = 1, a2 = 2, a3 = 3, a4 = 4, a5 = 5;
int b1, b2, b3, b4, b5;

void MyFunc()
{
    int c1 = 1, c2 = 2, c3 = 3, c4 = 4, c5 = 5, c6 = 6, c7 = 7, c8 = 8, c9 = 9, c10 = 10;
    a1 = c1; a2 = c2; a3 = c3; a4 = c4; a5 = c5;
    b1 = c6; b2 = c7; b3 = c8; b4 = c9; b5 = c10;
}

以上C语言代码的汇编代码如下

_DATA segment dword public use32 'DATA'
_a1 label dword   ; 定义一个名为_a1的标签,表示一个32位整数变量
    dd 1          ; 初始化_a1变量为值1
_a2 label dword   ; 定义一个名为_a2的标签,表示一个32位整数变量
    dd 2          ; 初始化_a2变量为值2
_a3 label dword   ; 定义一个名为_a3的标签,表示一个32位整数变量
    dd 3          ; 初始化_a3变量为值3
_a4 label dword   ; 定义一个名为_a4的标签,表示一个32位整数变量
    dd 4          ; 初始化_a4变量为值4
_a5 label dword   ; 定义一个名为_a5的标签,表示一个32位整数变量
    dd 5          ; 初始化_a5变量为值5
_DATA ends

_BBS segment dword public use32 'BSS'
_b1 label dword   ; 定义一个名为_b1的标签,表示一个32位整数变量
    db 4 dup(?)   ; 分配4字节的空间,但未初始化
_b2 label dword   ; 定义一个名为_b2的标签,表示一个32位整数变量
    db 4 dup(?)   ; 分配4字节的空间,但未初始化
_b3 label dword   ; 定义一个名为_b3的标签,表示一个32位整数变量
    db 4 dup(?)   ; 分配4字节的空间,但未初始化
_b4 label dword   ; 定义一个名为_b4的标签,表示一个32位整数变量
    db 4 dup(?)   ; 分配4字节的空间,但未初始化
_b5 label dword   ; 定义一个名为_b5的标签,表示一个32位整数变量
    db 4 dup(?)   ; 分配4字节的空间,但未初始化
_BBS ends


_TEXT segment dword public use32 'CODE'

_MyFunc proc near
push ebp              ; 保存当前函数的基址指针
mov ebp, esp         ; 将当前栈指针赋值给基址指针
add esp, -20         ; 分配20个字节的栈空间

push ebx              ; 保存ebx寄存器
push esi              ; 保存esi寄存器

mov eax, 1            ; 将值1赋给eax寄存器
mov edx, 2            ; 将值2赋给edx寄存器
mov ecx, 3            ; 将值3赋给ecx寄存器
mov ebx, 4            ; 将值4赋给ebx寄存器
mov esi, 5            ; 将值5赋给esi寄存器

mov dword ptr [ebp-4], 6    ; 将值6存储到基址指针减4的位置
mov dword ptr [ebp-8], 7    ; 将值7存储到基址指针减8的位置
mov dword ptr [ebp-12], 8   ; 将值8存储到基址指针减12的位置
mov dword ptr [ebp-16], 9   ; 将值9存储到基址指针减16的位置
mov dword ptr [ebp-20], 10  ; 将值10存储到基址指针减20的位置

mov dword ptr [_a1], eax    ; 将eax寄存器的值存储到_a1变量中
mov dword ptr [_a2], edx    ; 将edx寄存器的值存储到_a2变量中
mov dword ptr [_a3], ecx    ; 将ecx寄存器的值存储到_a3变量中
mov dword ptr [_a4], ebx    ; 将ebx寄存器的值存储到_a4变量中
mov dword ptr [_a5], esi    ; 将esi寄存器的值存储到_a5变量中

mov eax, dword ptr [ebp-4]   ; 将基址指针减4位置的值存储到eax寄存器
mov dword ptr [_b1], eax     ; 将eax寄存器的值存储到_b1变量中
mov edx, dword ptr [ebp-8]   ; 将基址指针减8位置的值存储到edx寄存器
mov dword ptr [_b2], edx     ; 将edx寄存器的值存储到_b2变量中
mov ecx, dword ptr [ebp-12]  ; 将基址指针减12位置的值存储到ecx寄存器
mov dword ptr [_b3], ecx     ; 将ecx寄存器的值存储到_b3变量中
mov eax, dword ptr [ebp-16]  ; 将基址指针减16位置的值存储到eax寄存器
mov dword ptr [_b4], eax     ; 将eax寄存器的值存储到_b4变量中
mov edx, dword ptr [ebp-20]  ; 将基址指针减20位置的值存储到edx寄存器
mov dword ptr [_b5], edx     ; 将edx寄存器的值存储到_b5变量中

pop esi               ; 恢复保存的esi寄存器
pop ebx               ; 恢复保存的ebx寄存器

mov esp, ebp          ; 恢复之前保存的栈指针
pop ebp               ; 恢复之前保存的基址指针
ret                   ; 返回调用方

_MyFunc endp

_TEXT ends
  • 栈清理
    先把esp存在ebp,函数返回之后,再把ebp的值存回esp,完成栈清理


    image.png
  • xormov 处理速度更快,编译器会自动优化

xor ebx ebx     ;将ebx寄存器清0,效率更高
mov ebx 0       ;将ebx寄存器清0
  • 循环语句转汇编
void MySub(){}
void MyFunc(){
  int i;
  for (i =0; i< 10; i++) {
    MySub();
  }
}
xor ebx, ebx      ; 将 ebx 寄存器的值设置为零(相当于 ebx = 0)
@4:
call _MySub       ; 调用名为 _MySub 的子程序(函数)
inc ebx           ; 将 ebx 寄存器的值加一(相当于 ebx = ebx + 1)
cmp ebx, 10       ; 比较 ebx 的值和 10
jl short @4       ; 如果 ebx 小于 10,则跳转到标签 @4 处(继续执行循环)
  • 线程不安全的原因
    下面一条C语言代码转换成三行汇编代码,没有原子性,线程1把counter*2的值读出来没来得及写入,线程2把counter原来的值读出来,这个时候就会出现大家都不想见到的情况
counter *= 2;    

mov eax, dword ptr[_counter];
add eax,eax
mov dword ptr[_counter], eax;

硬件控制方法

  • 现代高级编程语言很少能直接控制硬件,都是通过操作系统的系统调用控制硬件

  • 主机背后附带了用来连接显示器,键盘等外围设备的连接器,连接器的内部有用来和计算机交换数据的IO控制器,IO控制器中有用于临时保存输入输出数据的内存,这个内存就叫端口(port),比如键盘输入的内容都会暂存在键盘线和主机的连接处(IO控制器)中,然后才被CPU读取,端口的地址也成为IO地址,WINDOWS的IN/OUT命令使用IO地址进行通讯


    image.png
  • 蜂鸣器启动和停止
    计算机里面有个蜂鸣器,端口地址为61H(十六进制),这个地址大概存储一个字节(8位),其中低2位为1时发出蜂鸣,为0时停止蜂鸣,汇编代码如下,这段代码只能在Windows98以下的操作系统运行,之后Windows禁止了直接操作硬件的方式

; 启动
IN EAX, 61H     ; 从端口61H读取数据并存储到寄存器EAX中
OR EAX, 03H     ; 将寄存器EAX的值与00000011进行按位或运算,并将结果存储回EAX寄存器
OUT 61H, EAX    ; 将寄存器EAX的值输出到端口61H

; 停止
IN EAX, 61H     ; 从端口61H读取数据并存储到寄存器EAX中
AND EAX, 0FCH   ; 将寄存器EAX的值与11111100进行按位与运算,并将结果存储回EAX寄存器
OUT 61H, EAX    ; 将寄存器EAX的值输出到端口61H
  • IRQ (Interrupt Request) 中断请求
    主程序运行中,CPU收到硬件的中断请求,比如键盘鼠标输入,会保存好主程序的各个寄存器,把控制权交给硬件的中断处理程序
    外围设备的中断请求会使用不同于IO端口地址的中断编号
    中断控制器的功能是缓冲多个外围设备同时进行中断请求,使每个中断请求有序地传递给CPU

    image.png

    操作系统和BIOS都有提供响应中断编号的程序

  • 中断请求的顺序


    image.png
  • DMA (Direct Memory Access)
    DMA指的是主存和外围设备不通过CPU进行直接数据传送的技术,磁盘也用到了这个机制,估计是虚拟内存这块用到
    通过DMA,节省了CPU作为中间商赚差价的时间,大量数据可以短时间从外围设备的IO控制器传输到主存中
    使用了DMA的外围设备资源块会显示

    image.png

    假如多个外围设备都设定成同样的端口号,IRQ,DMA通道,计算机就无法正常工作并出现设备冲突的提示

  • 显示器中显示的信息一直都存储在VRAM(Video RAM)内存中,需要借助中断程序往VRAM写入数据,然后在显示器显示出来
    MS-DOC时代,VRAM是主存的一部分,A0000地址以后是VRAM区域,文字和图形的颜色最多只能16种,现代计算机把VRAM从主存独立出来,仍集成在主板上的叫集显,独立出去的叫独显

    image.png

  • GPU (Graphics Processing Unit)
    VRAM独立出来后,因为CPU的指令集处理图形运算效率太低,,GPU则被设计用于高并行的计算任务,特别擅长处理大规模图形数据和复杂的计算操作。
    显卡是由GPU和独立出来的VRAM组成的

让计算机思考

这章就算了

附录

  • 数据类型


    image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容