《深入理解计算机系统》
ddatsh
万丈高楼平地起,计算机系统就像程序员金字塔的地基。理解了计算机系统的构造原理,在写程序的道路上才能越走越远
道理很早就懂,一直没下定决心好好钻研,或许觉得日常工作中根本用不到这些,又或许是每次拿起书看到那些复杂的底层架构,看到存储器,寄存器,CPU,总线等等这些概念就头大
为什么这个框架要这么实现?源码实现涉及到多方面的的知识,设计模式,JDK的一些高级特性等等
每次看到很多不懂的东西,就深深的体会到现实与理想落差的痛苦,无力感
如果对计算机系统构造不太了解,可能平时写的程序都是错误的
一直以为两个正数的和或者积一定为正,但是用二进制补码表示的正数和或者积却不一定
程序员和编译器不能用(x-y<0)来代替(x<y),前者会产生溢出
甚至也不能用表达式(-y<-x)来代替,二进制补码表示中负数和正数的范围是不一致的。算术溢出是造成程序错误和安全漏洞的一个常见根源
最直接的原因。互联网公司面试,如阿里巴巴,腾讯。面试对于基础的东西要求的很严格,对这些东西的了解程度将直接决定面试成败
讲一下JVM的结构,TCP/IP三次握手、四次挥手,淘宝用户的数据怎么满足高并发?等
一、计算机系统漫游
Hello World 是如何运行的,存储设备层次结构及OS 的抽象概念
gcc hello.c
预处理cpp提取#include<stdio.h>代码到 hello.i
编译阶段cc1将hello.i成汇编hello.s
汇编阶段as将hello.s翻译成机器二进制 hello.o
链接ld将用到的printf.o链进来
程序执行,经历预处理器、编译器、汇编器及链接器处理,最终成为可执行文件
硬件组成
-
总线: 将字长数据在各部件传递
-
I/O设备: 通过不同的封装方式:控制器(设备本身或主板上的芯片组)或适配器(主板插槽的卡) 与 总线交互
-
内存和CPU: 程序计数器PC指向内存指令地址
指令做的操作:加载,内存值覆盖寄存器值;存储,反之;操作,ALU算术运算;跳转,指令本身抽取字,覆盖PC
程序代码和数据加载内存,cpu执行机器指令,字符串从内存复制寄存器,再复制到显存,输出
CPU L1,L2 等 cache是静态随机访问存储器SRAM硬件实现,高速缓存局部性原理(程序有访问局部区域数据和代码的趋势,大部分内存操作能在cache完成)
信息由位以及上下文表示,信息从I/O设备以位形式通过总线进入主存,由处理器从主存将信息取出处理
存储
存储设备金字塔结构
L1,L2,L3,内存,本地磁盘,远程存储
运行hello world时,shell和程序本身没直接访问显示器,磁盘或内存,由OS提供服务,通过几个抽象概念实现功能
OS 抽象
- 文件是对I/O设备的抽象表示
- 虚拟存储器是对主存和磁盘I/O设备的抽象表示
- 进程是对处理器、主存和I/O设备的抽象表示
Multics的失败催生借鉴了层次文件系统,用户级进程的shell的Unix,后用C重写
BSD分支增加虚拟内存和TCP;贝尔实验室自己的版本System V;Sun Solaris从两者衍生
Richard Stallman Posix标准化内核调用C接口,shell,线程,网络编程
进程
运行程序时,好像独占硬件,是 os 提供的假象,通过进程概念实现
实则并发运行,进程间指令是交错运行的
OS这种交错机制即上下文切换
上下文:OS保持跟踪进程运行所需的所有状态信息,比如PC和寄存器当前值,主存的内容
多处理器组织结构,4核,每核有L1和L2,共享 L3
超线程/同时多线程
一个CPU执行多个控制流,CPU某些硬件多个备份(PC和寄存器)
同时执行多线程原因:一个线程要等数据加载到高速缓存,CPU就可执行另一线程
流水线
如果一个指令完整执行(译码,校验等)要几十时钟周期,加上流水线不同指令头尾相连,接近一个周期执行一个指令
超标量
处理器比一个周期一条指令更快的执行速率时,super scalar
SIMD
提高 声音,视频等指令速度
虚拟内存
又为进程提供假象,独占内存
- 程序代码和数据:所有进程,代码从同一固定地址开始,0x08048000(32位)以及0x00400000(64位),紧接着是全局变量数据位置
- 堆:代码和数据区后紧随的是运行时堆。前者在进程开始运行时就规定了大小,而调用如malloc/free 等C 标准库函数时,堆可在运行时动态的扩展和收缩
- 共享库:如C标准库这样的共享代码和数据
- 栈:用户虚拟地址空间顶部,编译器用它实现函数调用,用户栈在程序执行期间可动态的扩展和收缩。调用函数时,栈增长;函数返回时,栈收缩
- 内核虚拟存储器:内核总是驻留在内存中,是操作系统一部分,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数
gcc -Wl,--verbose xx.c
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_H
文件
文件就是字节序列,即由0和1组成的序列
所有I/O设备,包括磁盘、键盘、鼠标、显示器都可以看成是文件
抽象的重要性
程序员无需了解内部的工作原理便可以使用这些代码
提供不同层次的抽象,隐藏实际实现的复杂性
Java中类的定义,C中的函数原型
二、信息存储和表示
进制
数据大小、寻址和字节顺序
数据类型
字符串, ASCII ,GB,UTF
布尔代数 ~非 &与 |或 ^异或
位移 « »
逻辑运算 || && !
原反补码
三、汇编、机器、高级语言
计算机由16位体系结构扩展为32/64位体系结构
Intel 用 “字”(word) 表示16位数据类型, 32 位 “双字”(double words),64 位“四字”(quad words)
movl、addl、subl、pushl等,l后缀就是表示数据格式,32位数值
寄存器
- 程序计数器,PC 下一条指令,常用%eip表示
- 整数计数器,一般存地址或整数,记录重要程序状态或存临时数据
- 条件码,最近执行的算数或逻辑指令状态信息,实现控制或数据流中的条件变化,比如用来实现 if 和 while
- 浮点
%eax,累加器
%ebx,数据的指针(偏移地址)
%ecx,计数器,循环次数
%edx,乘法积,或输入输出的端口地址(指针)
%esi,串操作源地址
%edi,串操作目的地址
%esp,寻址堆栈
%ebp,堆栈段数据区基地址
mov/movs/movz
push/pop
lea
inc/dec/neg/not
add/sub/imul/xor/or/and
sal/shl/sar/shr
汇编的流程控制
条件语句if-else,循环语句for,do-while,分支语句switch等,要求有条件的执行,根据数据测试结果决定操作执行顺序
条件寄存器 CF,ZF,SF,OF,JMP指令
过程,函数调用
过程在高级语也称函数,方法
过程调用涉及过程参数和返回值,和控制从代码一部分传递到另一部分
进入时为过程局部变量分配空间,退出时释放空间
IA32,只提供转移控制到过程和从过程中转移出控制这种简单指令。数据传递和局部变量的分配释放都通过操纵程序栈来实现
合理构建方法并调用,增加代码复用性,使代码结构更清晰
栈帧
程序执行时,大多数信息的访问都是相对于帧指针的!!!
程序栈来支持过程调用
栈传递过程参数、存储返回值、保存寄存器用于以后恢复,以及本地存储
为单个过程分配的那部分栈称为帧栈(stack frame)
帧栈两个端点,存在固定寄存器,起始在%ebp,结束在%esp
%ebp 为帧指针,%esp 为栈指针
过程实现中,参数传递及局部变量内存分配和释放都通过栈帧来实现
- 备份原帧指针,调整当前帧指针到栈指针
pushl %ebp
movl %esp,%ebp
2)建立的栈帧就是为被调用者准备的,被调用者使用栈帧时,需要给临时变量分配预留内存
subl $16,%esp
3)备份被调用者保存的寄存器当中的值,压入栈顶
pushl %ebx
4)使用建立好的栈帧,mov,push pop等
5)恢复被调用者寄存器当中的值,从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。因此在恢复时,大多数会使用pop指令
6)释放被调用者的栈帧,意味着将栈指针加大,具体做法一般是直接将栈指针指向帧指针(也可能是addl)
movl %ebp,%esp
7)恢复调用者的栈帧,其实就是调整栈帧两端,使当前栈帧的区域又回到了原始的位置
因为栈指针已经在第六步调整好了,此时只需要将备份的原帧指针弹出到%ebp即可
popl %ebp
8)弹出返回地址,跳出当前过程,继续执行调用者的代码
此时会将栈顶的返回地址弹出到PC,然后程序将按照弹出的返回地址继续执行。一般使用ret指令完成
过程的实现大概就是以上八个步骤组成,这些步骤并不都是必须的(大部分时候,开启编译器的优化会优化掉很多步骤),且6和7有时会使用leave指令代替
过程调用返回指令
call/leave/ret
-
call
call 指令目标参数,指明被调用过程起始指令地址
共做两件事,第一将返回地址(就是call指令执行时PC的值)压入栈顶,第二将程序跳转到当前调用的方法的起始地址
第一是为了为过程的返回做准备,第二是真正的指令跳转
-
leave
也是两件事,第一将栈指针指向帧指针,第二弹出备份的原帧指针到%ebp。
第一为了释放当前栈帧,第二为了恢复调用者的栈帧
-
ret
同样两件事,第一将栈顶的返回地址弹出到PC,第二按照PC此时指示的指令地址继续执行程序
其实也可以认为是一件事,第二是系统自己保证的,系统总是按照PC的指令地址执行程序
除了call外,leave和ret都与8个步骤有些不可分割的关系
call因为它发生在进入过程之前,第1步发生的时候,call指令往往已经被执行了,且已为ret指令准备好了返回地址