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 到调用前的状态。

例子2(call)#

例子3(ret)#

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

使用goto表达上述的汇编代码的意思:

goto Else之后,会继续执行Done标签之后的语句,也就是Done标签后的语句是无论判断是否失败都会执行的

编译器翻译#

Done表示这个C Code后面的所有逻辑代码

条件移动#

result是最后的结果。先把两个结果都算出来,result默认先赋值给其中一个。如果判断错误,则修改result(有条件的移动),否则直接返回。

  1. 用“提前计算 + 条件选择”取代“分支预测”,避免流水线清空。
  2. 只是 根据条件选择寄存器值,没有跳转,完全避免分支预测错误
  • 假设你知道的指令序列正在代码海洋中巡航,这些指令可以很顺畅的执行,因为他们使用了所谓的流水线技术。这意味着他们在完成下一个指令之前就开始执行下一个指令的一部分,实际上流水线能达到20条以上指令的深度,能达到的深度取决于提前获取的指令的条数,当完成一些指令时仍然有另一些指令留在流水线上,这就是我的海洋线或我的游轮类比。但突然他们到达一个分支会发生什么?他们会试着猜测分支结果,这被称为分支预测技术,猜测会运行哪个分支。在你熟悉的情况下,条件分支将被采用或将被落空,并且他们非常擅长预测,98%的时候他们都能猜对,所以他们甚至可以在路上预测suta曲线,并且开始朝着这个方向前进,只要猜测正确,就会非常有效率。但是,如果分支猜错了,你必须阻止它并转向另一个方向重新开始,在较差的情况下,可能需要40个指令40个时钟周期
  • 通过先执行两个分支来提高效率要容易得多,在最后一分钟,你要做的就是是否将值移入寄存器,这并不需要暂停整个处理器的执行然后重新选择分支执行

ConditionMoveExample#

CMOV是单条指令,不会打断 CPU 的指令流水线。

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 生成汇编 


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

数据类型#

  1. 多种整数数据类型(不区分符号与无符号,都以数字形式存储在计算机)
  2. 浮点(使用和整型不一样的寄存器组)
  3. 程序本身在x86中,是一系列字节
  4. 没有数组或结构,由编译器人工处理

指令#

每条指令做的事很有限(只做一件小事)

反汇编#

举例#

  • 变量的概念在汇编级别代码中完全消失(变成了寄存器或内存中某个东西)

使用gdb反汇编#