分类目录归档:深入理解计算机系统

深入理解计算机系统(第三版)/ CSAPP 杂谈,第12章:并发编程

Published / by sickworm / Leave a Comment
  • 基于进程的并发编程:accept 后 fork。优点:虚拟空间独立,不容易出 bug;缺点:创建进程开销大。需要处理 SIGCHLD 来回收僵死子进程。

  • 基于 I/O 多路复用:传递待读描述符集合给 select 函数,以此响应所有描述符的 I/O 事件。select 会修改描述符集合,改为可读子集返回,所以每次使用都要重新更新待读描述符集合。

  • 基于 I/O 多路复用的并发事件驱动:保存所有已连接的客户端,每次循环检测监听套接字,然后更新客户端集合,并执行相应操作

  • 状态机就是一组状态(state),输入事件(input event),和转移(transition)

  • 基于线程的并发:accept 后创建线程处理。但容易引入竞争(race)。

版权所有,转载请注明出处:
https://sickworm.com/?p=1670

深入理解计算机系统(第三版)/ CSAPP 杂谈,第11章:网络编程

Published / by sickworm / Leave a Comment
  • IP 协议提供基本的命名方法和递送机制
  • UDP 稍微扩展了 IP 协议(增加端口概念),使其从原来的主机间传送变成可以在进程间传送(通过端口区分不同进程),通讯粒度从主机变为进程
  • TCP 基于 IP 协议提供进程间可靠通信
  • 网络通信总是使用大端传输(网络字节序=大端)

  • Linux,Mac,Windows 都是使用 socket 通信。

int socket(int domain, int type, int protocol) // 创建套接字描述符,成功返回非负数描述符,失败为-1
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen) // 连接服务器,成功为 0,失败为 -1

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) // 连接客户端,成功为 0,失败为 -1
int listen(int sockfd, int backlog) // 等待客户端连接。此函数将主动套接字转化为监听套接字,成功为 0,失败为 -1
int accept(int listenfd, struct sockaddr *addr, socklen_t addrlen) // 等待来自客户端的连接,成功返回非负数连接描述符,失败为-1

int getaddrinfo(const char* host, const char* service,
    const struct addrinfo* hints, struct addrinfo** result)
// 用于主机名,主机地址,服务名,端口号的字符串表示转换成 addrinfo
// addrinfo 是一个列表,客户端调用 getaddrinfo 后需要遍历 result 这个列表,直到某个元素可以执行 socket 和 connect 成功
// host 可以是域名也可以是 ip 地址
// service 可以是服务名(http)或端口号
// hints 用于设置一些参数以便对返回的 result 列表做更好的控制

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
    char* host, size_t hostlen,
    char *service, size_t servlen, int flags)
// 用于 sockaddr 转换成 主机名,主机地址,服务名,端口号的字符串表示

// 简化版(非 Linux 内核内置)
int open_clientfd(char *hostname, char *port) // 客户端连接服务器
int open_listenfd(char* port) // 服务器监听端口
  • HTTP 请求由 request line,request header,空行 组成。
GET / HTTP/1.1  // method URI version
Host: www.aol.com

  • HTTP 有 GET,POST,OPTIONS,HEAD,PUT,DELETE,TRACE

  • HTTP 响应由 response line(1),response header,空行,response body,空行组成

HTTP/1.0 200 OK  // version status-code status-message
MINE-Version: 1.0
Date: Mon, 8 Jan 2010 4:59:42 GMT
Server: Apache-Coyote/1.1
Content-Type: text/html
Content-Length: 42092

<html>
...
</html>
  • CGI,Common Gateway Interface,通用网关接口
  • CGI 收到客户端请求后:解析参数,fork 子进程,子进程执行请求路径上的程序,使用 dup2 将标准输出重定向到客户端的已连接描述符。

版权所有,转载请注明出处:
https://sickworm.com/?p=1665

深入理解计算机系统(第三版)/ CSAPP 杂谈,第10章:系统级I/O

Published / by sickworm / Leave a Comment
  • 每个打开的文件,内核都保持着文件位置
  • Linux 每个进程都有当前工作目录
  • stat 和 fstat 可以读取文件的元数据(metadata)
  • readdir 以流形式读取目录内容
  • Linux 使用了 3 个数据结构表示打开的文件:
    1. descriptor table 描述符表。每个进程独立维护,通过打开的文件描述符索引
    2. file table 文件表。所有进程共享,持有 vnode 指针,文件位置,引用计数,引用计数为 0 时删除表项
    3. v-node table v-node 表。所有进程共享,包含 stat 结构的大部分信息。
  • I/O 重定向通过 dup2 函数实现。
  • 标准 I/O 库将打开的文件模型化为一个流(steam),对于程序员而言,一个流就是一个指向 FILE 类型的结构的指针。每个 ANSI C 程序开始时都有三个打开的流:stdin,stdout,stderr(0,1,2)
  • 类型为 FILE 的流是对文件描述符和流缓冲区的抽象。如第一次调用 getc 时,库会调用一次 read 函数填满缓冲区。
  • 执行输出函数后,若想使用输入函数,需要使用 fflush 清空缓冲区,或使用 fseek,fsetpos,rewind 重置当前文件位置
  • 执行输入函数后,若想使用输出函数,需要使用 fseek,fsetpos,rewind 重置当前文件位置,除非输入函数遇到了文件结束。

版权所有,转载请注明出处:
https://sickworm.com/?p=1639

深入理解计算机系统(第三版)/ CSAPP 杂谈,第9章:虚拟内存

Published / by sickworm / Leave a Comment
  • 所有程序共享内存资源,这容易造成很多问题。虚拟内存用于管理内存,协调各程序之间的内存占用和释放,但对程序来说无感知。
  • 物理寻址流程:CPU 执行加载指令时,生成一个物理地址,通过内存总线传递给主存。主存取出物理地址对应的内存,并返回给 CPU,CPU 将其存放在寄存器中
  • 虚拟寻址流程:CPU 执行加载指令时,生成一个虚拟地址,通过内存总线传递给主存,主存将其转换成物理地址。主存取出物理地址对应的内存,并返回给 CPU,CPU 将其存放在寄存器中。转换过程叫做地址翻译 address translation。
  • address translation 需要硬件和操作系统一起合作完成。CPU 中有专用硬件 MMU(Memory Managerment Unit),会利用存放在主存中的查询表(页表)来动态翻译虚拟地址。
  • 虚拟内存的基本思想:同一个数据可以有一个或多个地址,其中每个地址都选自不同的线性地址空间。线性地址空间是一个连续的非负整数集合,可以根据需要自由定义其大小和范围(线性空间)
  • 虚拟内存会被分割成固定大小的虚拟页,物理内存对应分割为物理页,大小与虚拟内存一致
  • 虚拟页存在三种状态:未分配,缓存(到物理页),未缓存(到物理页)。
  • 页表 page table 存放在内存(DRAM)中,记录虚拟页到物理页的映射关系。地址翻译硬件转换地址时都会读取页表,而操作系统负责维护这个页表,以及在磁盘和内存中来回传送页。

—- 20190113 —-

  • SRAM 缓存指的是 CPU 和 L1,L2,L3 和主存之间的缓存,DRAM 缓存指的是虚拟内存系统的缓存,他在主存中缓存虚拟页。
  • 虚拟内存优点:
    1. 简化链接:每个进程都可以使用相同的基本格式(包括 segment 组成,内存地址);
    2. 简化加载:向内存中加载可执行文件和共享对象文件变得简单。加载磁盘文件时是通过虚拟内存地址加载的;
    3. 简化共享:每个进程资源是隔离的,但只要将虚拟页面映射到同一个物理页面,就可以了安排多个进程共享这部分代码的一个副本,而不是每个进程都包含单独一个副本;
    4. 简化内存分配:进程申请额外的堆空间时(如 malloc),操作系统分配连续数字的虚拟内存页,并映射到不一定连续的物理页面中。
  • 虚拟内存的特性可以作为内存保护的工具。进程之间无法随意的访问对方的内存空间。虚拟内存还可以通过设置 flag 来控制内存的读写权限
  • 地址翻译过程:(命中)
    1. 处理器生成虚拟地址 VA,传给 MMU;
    2. MMU 生成 PTE 地址,通过高速缓存或主存请求得到它;
    3. 高速缓存或主存返回PTE;
    4. MMU 构造物理地址,并传送给高速缓存或主存;
    5. 高速缓存或主存返回请求的数据字给处理器。
  • 地址翻译过程:(不命中)
    1. 处理器生成虚拟地址 VA,传给 MMU;
    2. MMU 生成 PTE 地址,通过高速缓存或主存请求得到它;
    3. 高速缓存或主存返回PTE;
    4. PTE 有效位为 0,传递 CPU 的控制,让操作系统内核执行缺页异常处理程序
    5. 确定物理内存的牺牲页,如果该牺牲页已被修改则换出磁盘;
    6. 缺页处理程序页面调入新的页面,并更新内存中的 PTE。
    7. 返回到原来的进程,再次执行缺页指令,此时会命中,MMU 将返回请求的数据字给处理器。
  • 在 SRAM 缓存和虚拟内存共存的系统中,大部分系统使用的是物理寻址来访问 SRAM。这样设计可以让 SRAM 缓存保持简单。
  • PTE 也可以有缓存,来避免每次使用虚拟地址 MMU 都要查阅一个 PTE。PTE 缓存就在 MMU 中,叫 TLB
  • 内存映射是 Linux 把磁盘上的一个对象,关联起来并初始化一个虚拟内存区域的内容。
  • fork 原理是虚拟内存全部页面改为只读,新页面私有写时复制。
  • 内存分配实现:1. 隐式空闲链表。把大小和是否使用写在区块头,通过遍历链表(一般用数组实现),找出空闲区块。匹配策略有三种:首次适配,从头找到第一个适合的空闲块;下次适配,从上一次位置开始找;最佳适配,找到符合要求的最小空闲块。匹配后分割也有策略。合并(碎片整理)也有策略,可以是立即合并(释放时合并)和推迟合并(无空间时合并,通常会使用这个)。
  • 2. 显式空闲链表。把空闲块信息写在空闲块主体内(因为它是空闲的,所以可以用来存数据),信息数据结构是一个双向链表,记录上一个和下一个空闲块。管理方法可以是 LIFO,刚释放的块放入链表头部,这样释放块时间复杂度是 O(1),如果用了边界标记(末尾标记),合并的时间复杂度也是 O(1);也可以是地址顺序,这样首次匹配复杂度是 O(n),但内存利用率接近最佳匹配。显示链表有最小区块限制,一定程度上提高了内部碎片的程度。
  • 3. 分离空闲列链表。维护多个空闲链表,同一个链表的空闲块大小大致相等。
  • 4. 垃圾收集器根据内存的可达图,来判断孤立的内存块,进而回收

版权所有,转载请注明出处:
https://sickworm.com/?p=518

深入理解计算机系统(第三版)/ CSAPP 杂谈,第8章:异常控制流

Published / by sickworm / Leave a Comment
  • 异常控制流(Exceptional Control Flow,ECF)是操作系统为应用提供的一种访问处理器资源之外的能力,对应于嵌入式和CPU等硬件的中断概念。

  • 系统调用,进程管理,并发,IO 访问都属于异常控制流。

  • 异常(exception)是控制流的突变,用来处理处理器状态中的某些变化。异常通过事件(event)触发,有专门的异常表(exception table)用于事件的跳转。

  • 每种类型的异常都有唯一的异常号(exception number),有可能是处理器设计时分配的零除,缺页,内存访问违例,断点和算术运算溢出;也可能是操作系统分配的系统调用,外部IO设备信号。操作系统的异常号是在系统启动时初始化的。

  • 异常表的格式是:异常号->地址。

  • 异常的处理是在内核空间内,拥有访问所有资源的权限。

  • 异常分为,中断(interrupt,异步),陷阱(trap),故障(fault),终止(abort)。

  • 系统调用属于陷阱异常,用户程序想要调用服务n时,就执行处理器提供的特殊指令 syscall n。这会触发异常处理程序,程序会解析参数,并调用合适的内核程序。普通函数调用则无法进入内核空间,也就无法访问特殊的资源。

  • 终止异常是硬件问题,如 RAM 校验等不可恢复的错误,只能直接终止程序

  • 段故障(segment fault)通常因为程序访问了受限的内存块导致的,如读未定义的虚拟内存,写只读内存等。

  • 并发流(concurrent flow)与处理器核心数无关,只要时间上重叠即可,多个进程以并发流形式运行成为多任务(multitasking);并行流(parallel flow)必须是在不同的处理器或计算机上同时运行。也就是说,并行流一定是并发流,反之不一定。

  • 模式位(mode bit)在处理器层上提供内核模式和用户模式的内存空间访问范围控制。没有设置模式位时,就处于用户模式,此时不允许使用特权指令(privileged instruction),如停止处理器,改变模式位,或发起一个 I/O 操作。

  • 上下文切换(context switch)是操作系统基于异常来实现的异常控制流,用于实现多任务。内核为每一个进程(process)维持一个上下文,具体内容包括寄存器,用户栈,内核栈,和各种内核数据结构如页表,当前进程信息的进程表,当前进程打开的文件的文件表。

  • 内核使用调度器(scheduler)对进程做上下文切换,从而切换当前运行的进程。切换时机由调度器决定,比如时间片用完,sleep,请求磁盘数据后。

  • init 进程是所有进程的祖先,如果父进程在回收它的僵尸子进程前就终止了,则由 init 进程回收它们。waitpid 可以用来等待自己的子进程终止或停止。

——- 20190101 ——–

  • fork 创建一个新的进程,execve 在原有进程上执行新的程序的 main 函数。

  • Linux 信号允许进程和内核中断其他进程。信号可以理解为一条消息,一个事件。

  • 发送信号的程序可以是内核,另一个程序,自己

  • 接收信号可以用 signal handler 来捕获这个信号(相当于嵌入式的中断函数),或者忽略。SIGKILL 和 SIGSTOP 信号无法被捕获和忽略。发出但没有被接收的信号叫 pending signal。一个类型最多只能有一个待处理信号,后面的同类型信号将会被丢弃。这个特性可以让程序有选择性的阻塞接收特定信号,做到不重复处理的功能。

  • 可以大量向 process group 进程组发送信号,父进程和子进程属于同一个进程组。使用 setpgid 设置自己的 pid 进程组

  • 阻塞信号分为两种:隐式和显式。隐式是指处理信号 s 的程序正在运行且未返回时新的信号 s 会变成待处理而没有被接收(注意最多只能有一个 s 待处理信号)。显式是使用 sigprocmask 明确的阻塞和接触阻塞选定的信号。

  • 信号处理程序需要:

    1. 尽可能简单,如设置一个flag。否则会被阻塞;
    2. 只调用异步信号安全的函数,他们有可重入(例如只访问局部变量),或不能被中断的特性
    3. 保存和回复 errno,在进入处理程序时吧 errno 保存在局部变量中,并在返回时恢复它(如果你的处理程序会调用会修改 errno 的方法的话)
    4. 访问全局变量时,阻塞所有信号。否则会出现竞争导致不可预知的结果
    5. 使用 volatile 声明全局变量。优化编译器有可能会优化掉周期读取全局变量的代码,进而使用缓存值。vollatile 会让编译器不要缓存这个变量;并强迫代码每次引用全局变量时,强制从内存中读取。
    6. 使用 sig_atomic_t 来声明标志,它是原子读写的(不可中断的)。这样就不需要暂时阻塞信号。原子性只能保证单个读写,不能保证 falg++ flag+=10 这样的操作。
  • signal 函数不同系统的语义可能不一样。比如每次触发信号处理之后可能需要再次调用 signal 函数,否则不会再次触发。

  • setjmp 和 longjmp 函数可以实现非本地跳转 nonlocal jump,即从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。

  • setjmp 在 env 缓冲区保存当前调用环境,以供后面的 longjmp 使用,返回 0。longjmp 调用后 setjmp 再次返回,此时返回值非 0(类似 fork)。该功能可用于捕获异常信号

  • C++ 和 Java 的异常机制是 setjmp 和 longjmp 的更加结构化的版本。try catch 类似 setjmp,throw 类似 longjmp。

深入理解计算机系统(第三版)/ CSAPP 杂谈,第7章:链接

Published / by sickworm / Leave a Comment
  • 链接器主要完成符号解析和重定位两个任务。

  • 目标文件有三种形式:可重定位目标文件(.so);可执行目标文件(.exe),共享目标文件(.so)。

  • linux x86-64 的可重定位目标文件使用 ELF 格式。ELF 头的前 16 字节描述文件对应系统的字的大小和字节顺序,后面还有头的大小,目标文件类型,机汽类型,各 section header 的文件偏移,以及它们的大小和数量。

  • 一般 ELF 包含以下几种 section:

    • .text:可执行机器码
    • .rodata:只读数据,如字符串常量和 switch 跳转表
    • .data:已初始化的全局和静态变量
    • :bss:未初始化或初始化为 0 的全局和静态变量。不占空间,仅仅为占位符。运行时在内存动态生成。
    • .symtab:符号表,存放定义和引用的函数与全局变量的信息。使用 STRIP 命令可以去掉符号表。
    • .rel.text:.text 中位置的列表,是重定位信息。链接器把目标文件组合起来的时候,需要修改这些位置,以让各个目标文件链接起来。一般来说,修改的是外部函数或者引用全局变量的位置,调用的本地函数的位置则不需要修改。
    • .rel.data: 引用或定义的所有全局变量的重定位信息。
    • .debug:调试符号表。用 -g 选项编译的时候才会得到这张表。
    • .line:源程序的行号与 .text 机器码的对应关系。用 -g 选项编译的时候才会得到这张表。
    • .strtab:字符串表,包括 .symtab,.debug,节头名字。以 null 结尾的字符串序列。

—- 2018.12.14 —-

  • .symtab 存储各 section 重定位信息

  • 弱全局符号是未赋值的全局符号,会被强全局符号覆盖,包括类型,所以容易导致类型不匹配的细微的bug;强全局符号是初始化的全局符号,会互相冲突。弱全局符号分配在 COMMON section 中,强全局符号分配在 .bss 中。

  • 静态库用于共享重复的代码,链接器仅会拷贝需要的函数。也可以通过参数拷贝所有函数。

  • gcc 的静态库链接是按顺序进行的。遇到目标文件 .o 时会把未定义和已定义的符号保存起来,遇到存档文件 .a 时,除了前面的操作,还会把 .a 的成员符号与未定义的符号比较,把匹配的成员符号对应的 .o 链接起来。这样的话因为是顺序的,如果把静态库放在前面,则会错过后面目标文件的匹配,从而在链接完所有文件,却还是有未定义符号,结果编译报错。

  • 所以一般做法是静态库文件放在最后。如果 .a 库相互之间也有依赖,则需要将 .a 库排序。

版权所有,转载请注明出处:
https://sickworm.com/?p=500

深入理解计算机系统(第三版)/ CSAPP 杂谈,第6章:储存器层次结构

Published / by sickworm / Leave a Comment
  • SRAM 贵,稳定,集成度低,用于高速缓存存储器

  • DRAM 较便宜,不稳定,集成度高,需要定时重新读写和纠错码,用于主存和帧缓冲区

  • DRAM 的存储单元(超单元)以二元阵列排列而不是线性排列,这样可以节省管脚。请求某个超单元先发送行,此时会将行缓存到内部行缓冲区;然后发送列,此时将该行该列的超单元数据返回给请求者。传统的 DRAM 会将剩余的数据丢掉,而 FPM DRAM会缓存整行。这两种DRAM早就已经停产了,现在主流是 DDR3/4 SDRAM。

  • 可擦写编程器 EEPROM 掉电数据不丢失,主要用于存储数据,如闪存(U盘)

  • 一般的程序都具有良好的局部性,即访问的数据都是在一个较密集的区间内,这样可以提高访问效率。但局部性也导致容易受到栈溢出攻击。印象中新版本的 Linux 内核有降低局部性,防止栈溢出攻击的策略。

  • 存储器层次结构的本质是,每一层存储设备都是较低一层的缓存。

  • 高速缓存步骤:1. 组选择(一般用内存中间的位避免相邻的内存分到同一个组) * 2.行匹配 3. 字抽取(返回)

  • 直接映射高速缓存命中遵循定的策略,在真实的程序中很常见,会导致令人困惑的性能问题。例如反复调用两个相邻的内存块(两个数组),而两个内存块映射的组索引恰好一样(块大小为2的幂时尤为容易出现),就会导致冲突不命中,只能每次重新加载。解决方案可以是稍微扩充一下内存块大小。直接映射高速缓存限制是只能有1行。

  • 组相联高速缓存每组可以有多行,冲突不命中的问题得到缓解。

  • 缓存经常会用到 write-back 机制,即更新的数据不会立刻写入到下一层,而是每隔一段时间写一次,以获得更高的性能。L1 对 RAM,RAM 对 ROM 都会用到。write-allocate 是当需要写的时候,如果没有匹配缓存,就先加载进来,而不是 write-through 直接写进去。write-back 和 write-allocate 经常一起使用。

  • i7 每个核心都有独立的 L1 i-cache (instructment) d-cache(data) 和 L2 unified cache。所有核心共享 L3 cache。L1 4周期,32kb,64组64块大小;L2 10周期,256KB,8行512组64块大小;L3 40-75 周期,8mb,16行8192组64块大小

版权所有,转载请注明出处:
https://sickworm.com/?p=494

深入理解计算机系统(第三版)/ CSAPP 杂谈,第3章:程序的机器级表示

Published / by sickworm / Leave a Comment
  • x86-64寄存器起名:8位,%al;16位,%ax;32位,%eax;64位,%rax

  • x86-64寄存器作用:

  • %rax: 返回值
  • %rbx: 被调用者保存
  • %rcx: 第4个参数
  • %rdx: 第3个参数
  • %rsi: 第2个参数
  • %rdi: 第1个参数
  • %rbp: 被调用者保存
  • %rsp: 栈指针
  • %r8: 第5个参数
  • %r9: 第6个参数
  • %r10: 调用者保存
  • %r11: 调用者保存
  • %r12: 被调用者保存
  • %r13: 被调用者保存
  • %r14: 被调用者保存
  • %r15: 被调用者保存

  • 操作数分三种:立即数,寄存器,内存引用

  • 汇编立即数表示:以$开头,跟着标准C表示法,如$-577,$0x1F。不同的指令允许的立即数范围不同

  • 寻址方式:立即数寻址,立即数作为值;寄存器寻址,寄存器值作为值;绝对寻址,立即数对应内存地址的内存值;间接寻址,寄存器值对应内存地址的内存值;变址寻址,运算表达式对应内存地址的内存值

  • 表达式:(s取值1,2,4,8)

  • $Imm: Imm,立即数寻址

  • ra: R[ra],寄存器寻址
  • Imm: M[Imm],绝对寻址
  • (ra): M[R[ra]],间接寻址
  • Imm(rb): M[Imm + R[rb]],(基址+偏移量)寻址
  • (rb, ri): M[R[rb] + R[ri]],变址寻址
  • Imm(rb, ri): M[Imm + R[rb] + R[ri]],变址寻址
  • (,ri, s): M[R[ri] * s],比例变址寻址
  • Imm(,ri, s): M[Imm + R[ri] * s],比例变址寻址
  • (rb,ri, s): M[R[rb] + R[ri] * s],比例变址寻址
  • Imm(rb,ri, s): M[Imm + R[rb] + R[ri] * s],比例变址寻址

—- 2018.11.11 —-

  • movb, movw, movl, movq 分别表示把1,2,4,8字节的数据从源位置复制到目标位置。x86-64 限制源位置和目标位置不能两个都是内存。大部分指令都会有这4种变种,但如pushq,popq,leaq等指令没有

  • movz,movs是0扩展和符号扩展指令,将较小的数从寄存器或内存转移到寄存器中。

  • x86-64中,栈中低地址是栈顶,高地址是栈底。pushq时%rsp减小,popq时%rsp增大。

  • leaq S,D: 将S所表示的地址值赋值给D,经常用来做一些简单的算法。比如z = x+4*y可能表示为leaq (%rdi, %rsi, 4), %rax

  • 一元操作中,操作数既是源也是目的。操作数可以是内存地址。

  • 二元操作中,源操作是第一个,目标操作是第二个。如subq %rax, %rdx等价与%rdx = %rdx – %rax。如果第二个操作数是内存地址,则必须读出,执行,再写入。注:此时应该会生成多条机器指令,则该操作不是原子操作。否则i++可以写成addq $1, (%rax)岂不是原子操作?所以推断该操作不是原子操作。

  • sarq,shrq表示算术右移和逻辑右移。salq, shlq表示算术左移和逻辑左移,但由于补码的特性,他们的操作结果是一样的。

*x86-64还为128位操作提供有限支持,当imulq和mulq为双操作数时,是64位乘法;当为单操作数时,另一个乘数将视为%rax,而结果将存放在%rdx(高64位),%rax(低64位)中。

—- 2018.11.18 —-

  • 条件码用于控制。最常用的四个是,CF: 进位标志,ZF:零标志,SF:符号标志,OF:溢出标志

  • 通过SET指令访问条件码,入sete/setz %al就是将ZF设置到%eax的最低byte上(0或1)

  • jmp表示跳转指令,一般汇编用一个标号来指定,如jmp .L1。也可以利用寄存器,如jmp *%rax表示跳转到%rax的值的地址,jmp *(%rax)表示跳转到%rax值对应的内存值的内存地址去。和SET一样,也有je,js,jle等指令

  • GCC当switch的情况较多,且跨度较小的时候,会使用地址跳转表来翻译switch

  • 当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就回在栈上分配空间,这个部分称为过程的栈帧

  • call指令用于函数调用,此时将会把将紧跟着当前地址后面的那条指令的地址压入栈,并吧要跳转的函数地址设置到PC。退出时用ret。

  • 局部数据存到需要内存中的几种情况:

    1. 寄存器不够用
    2. 对局部变量使用了地址运算符&
    3. 某些局部变量是数组或结构体(如果优化了则不一定)
  • 数据对齐可提高软件性能,减少读的次数和额外的处理。无论数据是否对齐,x86-64都可以正确工作。汇编中在文件头部声明.align 8来保证后面的数据的起始地址都是8的倍数。

  • 对于结构体,字段之间也可能会存在间隙,以保证每个结构元素都满足它的对齐需求。

  • 对齐原则是任何K字节的基本对象的地址必须是K的倍数。

  • Linux上最新版的GCC已经会讲栈地址随机化,使得栈溢出攻击变得更加困难

版权所有,转载请注明出处:
https://sickworm.com/?p=471

深入理解计算机系统(第三版)/ CSAPP 杂谈,第2章:信息的表示和处理

Published / by sickworm / Leave a Comment
  • gcc编译可以用 -std 和-ansi 来选择C语言规范版本,默认-std=c89,外加一些C99,C11特性

  • 大小端可通过 int* 强转为 char* 来判断得出

  • 一般C语言的 char 范围是-128~127,但其实C语言规范只要求最小的可取值范围为-127~127。类似还有 short,int,long,不过它们字节数要求是16位机器的要求。int32_t,int64_t C语言规范要求和C语言实现基本一致。#无用但有趣的冷知识

  • #define INT_MAX 2147483647
    #define INT_MIN (-INT_MAX – 1)
    这里没有简单地将INT_MIN赋值成-2147483647,是因为-2147483648对于编译器而言是个表达式,而2147483648对于32-bit整数是无法表示的

  • 扩展数字:有符号转无符号,按bit解析;扩展字节,正数补0,负数补1

  • 截断数字:无符号转有符号,按bit解析;压缩字节,保留符号位,其他直接截取

—- 2018.10.23 —-

  • 补码乘法:将补码看作无符号正整数,相乘后截断结果,结果即为乘法结果。e.g.

(13 x -7) mod 2^8 = -91 = 10100101

-7 = 11111001b = 249(看作无符号)

(12 * 249) mod 2^8 = 165 = 10100101 = -91(看作补码)

  • 浮点数基本都按照IEEE 754规范实现。1位用于表示正负,k位用于表示尾数,代表精度为1/2^k,n位表示阶码,代表绝对值取值范围为1 * 2^-(n-1) – 1, 2^(n-1)),分别是阶码取1和2^n-1时的值。尾数可任意取值时,绝对值范围为[1 * 2^-(n-1) – 1, 2 * 2^(n-1)))。阶码全0全1另有它用。具体看图2-36。

  • 单精度浮点数符号位 1,尾数位23,阶数位8;双精度位1,52,11。所以单精度的精度为1/2^23,绝对值取值范围为[2^-126,2^127]。尾数默认第一位为1,这样可以不存储在bits中,从而提高一位精度。但也因此不能表示0(阶码全0可表示)。

  • 阶码全0时可用于表示0,此时还会有+0.0和-0.0。阶码全1,尾数全0可表示正无穷和负无穷。尾数非全零表示NaN(Not a number)。

版权所有,转载请注明出处:
https://sickworm.com/?p=450

深入理解计算机系统(第三版)/ CSAPP 杂谈,第1章:计算机系统漫游

Published / by sickworm / Leave a Comment

本书第1章向我们介绍了信息的定义,以及程序是如何运行在计算机上的。

信息就是位+上下文

信息的本体是一连串的0101010101的bits,但是bits可以被解析为不同的含义,如何被解析就取决于上下文。

举个生活中的例子。“你好烦啊”如果是在一对情侣的温馨时刻中出现,那这句话可以被理解为“你个烦人的小妖精搞得我不要不要的”。而如果出现在母亲指责孩子,并且喋喋不休的情景下孩子的顶嘴,就应该理解为“你不要再说了我听不进去”。这就是上下文的作用,同样的内容,会被解析为不同的信息。

本文用到的例子是一串bits,如果代码想解析你为无符号整数,那这串bits就是正整数的信息;如果想解析为浮点数,那就是小数的信息,且与正整数代表的值可能完全不一样了。放大一点说,代码执行时当前的寄存器值,全局变量的值,栈帧的状态;程序运行时内存缓存的值,数据库的数据,都是它们的上下文。

程序的编译

现代编程语言具有很高的抽象程度,这是为了让人们更高效率的编写业务逻辑。而所有的程序代码都会在最终以机器码的形式执行,因为计算机只认识机器码。程序代码转换为机器码的过程大致都是如下的流程:

预处理:解掉一些语法糖,和调整部分代码,使其更方便的被编译器编译

编译器:将预处理后的代码进行编译,得到中间代码。中间代码不一定是汇编语言,也可能是C语言或其他语言,这取决于编译器的实现,一切为了效率。近几年比较流行的llvm,很多语言都基于它实现了编译器,而llvm也有它专用的中间语言IR。

汇编器:如果你实现的中间语言是汇编,那就是汇编器,否则就是其他器。这部将中间语言转换为可执行代码。可执行代码可能是计算机机器码,也有可能是某些依赖虚拟机的语言的虚拟机机器码。

链接器:代码以文件为单位进行编译。如果引用了文件外的方法,变量,则会在编译时留下一个“接口”,因为它暂时无法被确定。在链接阶段将会确定这些“接口”。

系统的硬件组成

讲了系统的硬件组成,直接看就行。比较有意思的是多层缓存机制。

进程,文件,网络,并发和并行

讲了计算机的一些代码执行和IO通信的机制。

版权所有,转载请注明出处:
https://sickworm.com/?p=440