深入理解计算机系统

16SystemLevelI_O

概述 #

从程序员的角度来讨论这个话题 标准的Unix库:printf和scanf
低级IO,直接面向操作系统

Unix I/O概括 #

用文件(一堆二字节序列)来描述很多抽象的事物,比如I/O设备,打字机;网络连接(套接字),消息是写入套字节传送,读取套字节接收;

文件还有个属性:读取位置;网络套接字不允许在时间上跳转,只能在数据包进入时对其进行读或写

文件类型 #

文件、目录、套接字(作为发送和接收)、应用程序之间接收发送(视频不讲)
符号链接(多指针指向同一文件等)

普通文件 #

  • 有些应用程序会区分文本文件和二进制文件(不是在操作系统级别区分,而是在更高级别)
    • 主要区别:文本文件只有标准的ASCII字符或者对非英文字符编码的Unicode字符
    • 二进制文件是图像实际目标代码音视频文件等所有其他的文件(这类文件有一个字节序列直接是某种形式编码的数字)
    • 文本文件:新行 符号,0xa;windows:\r\n 0xd 0xa,Linux:\n 0xa

目录 #

目录名 #

打开文件 #

  • 以指定方式打开文件,这里会返回一个文件操作符
  • O_RDONLY(什么模式下访问文件)

  • 同时打开的文件数量有限

每次进行系统调用时都应该检查返回值,看看是否存在错误,并采取适当方法处理错误

  • 有几种和终端关联的文件:标准输入、标准输出、标准错误

关闭文件 #

多线程中容易出错,关闭已经关闭的文件

读文件 #

读入若干字节数(最小是1)

...

21NetworkProgramming01

主旨 #

真正的理解和熟练地编写网络软件
利用与网络编程有关的c语言api
如何建立一个网络系统

客户端-服务端传输 #

网络主机的硬件结构 #

当你发送信息时,实际上是通过向一个叫做网络的虚拟文件写入数据来实现的;而当你想接收数据时,通过读取该文件来实现

08MachineLevelProgramming04Data

概述 #

数据表示
目前都是操作整数、长整数、指针(标量数据),而不是聚合形式的数据

数组、结构

在机器内存中的表现方式

数组不过是一堆字节,但是这些字节的集合,是在连续位置上存储的;结构也是如此,作为字节集合来分配的

数组分配 #

x,x+4,x+8这些都是内存地址(机器代码会这么处理)(不是指数组下标)

T A[L];

  1. 分配足够的存储字节来保存整个数组
  2. 像对待指针一样对待标识符A

对于找val[4]这个元素,汇编中,编译器知道指针类型后,会自动写出合适的缩放因子,不需要程序员手动处理

例子 #

访问数组的例子 #

循环处理数组元素 #

  • ZLEN=5,cmpq b,a (比较的是a和b),jbe 低于或等于;这里就是说如果%rax 小于或等于4,则跳转到.L4

    程序解释:先比较 %rax和$4,如果小于或等于0,则跳转到.L4

  • addl $1,(%rdi,%rax,4),首先从内存中读取原始值,进行加法运算,然后将结果放回内存中

数组和指针之间真正区别 #

当你在c中声明一个数组时,你既在分配空间,分配某个位置的空间,同时,也在创建一个允许在指针运算使用的数组名称。当你只是声明一个指针时,你所分配的只是指针本身的空间,没有给他指向的东西分配空间

例子2 #

  • 【从内往外读】A3是一个指针,指向(由三个int组成的数组)
  • A3可能是一个空指针,即 *A3可能为空

  • 优先级排序:(),[],*
  • A4是三个元素的数组,这些元素是指针,而这些指针指向int

多维数组 #

07MachineLevelProgramming03Procedures

概述 #

procedures:面向过程编程中的函数、过程,或者,面向对象中的方法
abi(应用程序二进制接口): application binary interface。它要求所有Linux程序、编译器、操作系统、系统所有不同部分,都需要对如何管理机器上的资源有一些共同的理解。机器程序级别的接口。

  • ABI关注的是二进制代码如何在实际硬件上执行和交互
  • Windows、Linux 和 macOS 的 ABI(应用程序二进制接口)在核心设计上有显著差异,这导致它们的二进制程序通常无法直接跨平台运行。

控制(函数调用) #

  1. 将控制权转移给另一个函数,且函数执行完毕后返回控制权
  2. 如何传参,以及被调用函数如何将结果传给调用函数
  3. 被调用函数中的局部变量分配问题
  4. 尽量减小过程调用中的开销(对于程序员,良好的编程习惯: 尽量多功能多函数)

控制权转移 #

  • stack(栈):其实只是普通内存的一块区域
  • 对于汇编层面的程序员来说,内存只是一个巨大的字节数组
  • 用栈来管理过程调用与返回的状态
  • 利用栈后进先出的特点:调用函数时需要一些信息,从函数返回需要丢掉一些信息。

pop #

这是三个操作复合成一个操作(先读取,后递减栈指针,最后存储值)

栈的释放只是移动栈指针,没有其他特殊操作。数据还在内存中,只不过不是栈的一部分了。

push #

  • 被push的参数,也必须是寄存器,或立即数。(因为不允许直接从内存传递数据到内存)
  • 这也是个复合操作:获取操作数,递减栈指针(而且,内存读一个数据,数据的地址指的是它的(最)低地址),最后写入值

例子1 #

  • call和ret引用了栈的思想
  • 局部变量的存取通过 %rsp 偏移(%rsp-8,%rsp+8之类)实现,不会直接修改 %rsp

调用前将返回地址压入栈中,然后去执行函数,调用后从栈顶(%rsp)取出调用函数前存的下一条指令的地址,并将%rip(程序计数器[Program Counter, PC]设置为该值
注意:在函数返回前,需通过 add $N, %rsp 或 leave 指令(相当于 mov %rbp, %rsp + pop %rbp)恢复[很重要!!] %rsp 到调用前的状态。

...

06Machine-LevelProgramming02Control

概述 #

本节主要学习如何控制机器级别指令的执行顺序
基本条件语句(条件码,条件分支)、循环(循环)、switch语句

ProcessorState #

1.条件码 #

  • CF(无符号):进位(溢出)
  • ZF:刚才的计算结果为零
  • SF(有符号):结果的最高有效位为1(负值)
  • OF(有符号):进位(溢出)

这些标志一般情况下会被忽视,当进行条件操作时会被关注

显式设置1 #

显式设置2 #

testq b,a相当于计算 a&b

如果testq a,a。那么只有当a为零时,ZF才会被设置

读取条件码 #

根据条件码的某种组合,将一个字节设置成0或1

  • 最简单的是,设置ZF条件码的值sete %al(ZF是什么就设置成什么)

直接设置寄存器的最低字节 #

该操作不会影响其他7个字节

条件相关例子 #

  • %eax是%rax的低四位
  • mov指令,从单字节到四字节的零扩展指令
  • 前两步骤保证了低四字节的零扩展;x86-64保证:任何计算结果是32位(四字节)的计算,会把寄存器其余32位设置为0(高四字节)。但是,如果是单字节,或双字节,只会影响这个单字节或双字节的结果。

跳转指令 #

包括无条件跳转/有条件跳转

传统分支例子 #

jle .L4 表示如果条件码结果表示是 y <=x,则跳转到 .L4

...

05MachineLevelProgramming01Basic

概述 #

计算机为了运行程序,会执行一串独立的指令
两种形式的机器语言:

  1. 在计算机上运行的实际目标代码(字节,二进制01)
  2. 汇编代码(编译器的目标,文本格式)

不写汇编,但需要学习:

  1. 编译器产生的结果(汇编及目标代码)与文本代码关联
  2. 低级代码(汇编、二进制)如何实现高级别程序构造
  3. 过程函数、结构体、数组在机器语言中的实现

课程选择64位版本的Intel指令集
为什么叫x86处理器16位微处理器(也称CISC复杂,与之对应的是RISC精简),因为一开始的处理器是8086,接着8286,8386…
需要更加关注–gcc编译生成的代码长什么样

x86进化 #

多核处理器 #

另一个公司:AMD

教学范围 #

哪些处理器 #

  • ARM(AcornRISCMachine),比x86机器功耗更低
  • x86

定义 #

  • 指令集
  • 寄存器:非常小的内存位置
  • 机器指令

(无法直接操作缓存(没有这个概念),是机器级别的概念)

将c源代码转换成目标文件 #

汇编简介 #

gcc -Og -S sum.c #-Og 调试级别的优化过的(英文字母大写O); -S 生成汇编 


带点的这些,是给调试器用的(用来定位);也有一些是给链接器用(告诉它这是个全局定义的函数)

数据类型 #

...