【第 3 周】深入理解计算机系统共读心得体会

请大家以回帖的方式将你在共读第一周的心得体会用你自己的话表达出来。

样例:

所在小组

第一组

组内昵称

张三

你的心得体会

可以基于不同知识点进行,有更新请在原回贴更新,每人每周只发一个帖子

一段自己的阐述

第二段自己的阐述


本次是 TalkGo 读书会第三期的第三周,本次共有 24 人未完成(或超时)打卡,其中第四组共8人全组未完成打卡(原因是组长收集之后再打卡,超过限定的时间了),另外还有6个人也是超时打卡。

静默组:

hellolinux、Mr_李冲

第一组:

Liam、Gerald

第二组:

叶王

第三组:

MrTrans、Hector

第四组:

全部未打卡:Lu GH、志林、不出图不改名!、Helios、Zzde、彳亍、murphy、佳佳

第五组:

王传义、蒋亮亮、孙恒、郑伟钊、张学广

第六组:

黄永平、蒋权

第七组:

Hayden、K

所在小组

第七组

组内昵称

杨文

你的心得体会

  1. 能够理解汇编代码以及它与原始 C 代码的联系,是理解计算机如何执行程序的关键。

%rip 程序计数器
%rbx
%rdx
%rax
3.
pushq 表示应该将寄存器 %rbx 的内容压入程序栈
4. 机器执行的程序只是一个字节序列,是对一系列指令的编码。
4. movq 表示
5. objdump
6. subq
7. addq
8. ret
9. xorl
10. je
11. 很多库函数都有一个属性,不需要告诉它们目标缓冲区的大小,就产生一个字节序列,比如:strcpy,strcat,sprintf

对抗缓冲区溢出攻击:

  1. 栈随机化,让程序在每次运行时都有变化,在Linux 上这个技术被称为 ASLR(address space layout randomization)
  2. 栈破坏检测,C 语言中没可靠的方法防止对数组的越界写,但是可以在造成任何有害结果之前,尝试检测到它。
  3. 限制可执行代码区域

所在小组

第七组

组内昵称

吴奇驴

你的心得体会

  • 条件控制和条件传送:条件传送不一定就能提高代码的效率,在处理带分支的代码时候,编译器并没有足够的信息来进行可靠的分支预测,GCC在只有当两个表达式都容易计算的时候才会使用条件传送(书里那么说的);

  • do-while、while、for:C语言的程序都可以先转为带goto的实现,还可以进一步在汇编种由条件测试跳转组合来实现;

  • switch:使用了跳转表结构;

  • 堆栈:栈;

  • 过程:函数、方法、子例程、处理函数;

  • 帧:x86-64过程需要的存储空间超过寄存器的大小时候就会在栈上分配空间来存储,这个部分就是过程的栈帧;

所在小组

第三组

组内昵称

h0n9xu

你的心得体会

参见这里

所在小组

静默组

组内昵称

Tang_D

你的心得体会

  • 汇编的语句如move往往会带上字节的大小如 b-1个字节 w-2个字节 l-4个字节 q-8个字节
  • 汇编提供16个通用寄存器,有些寄存器具有特殊作用,如rax表示返回值,rsp表示栈指针。
  • 汇编的数据传输可以将寄存器转移到内存中,但是不可以直接从内存转到内存,只能通过寄存器进行中转。数据传输可以使用立即数,寄存器表示,以及内存地址中的值进行传输
  • 栈的地址增长方向是向地址低的方向增长,可以通过pushq,popq保存寄存器的值。
  • 加载有效地址 leaq 直接对寄存器里的值进行运算,可以用来计算代码中的符号运算,以及计算地址
  • 汇编中的运算,command S D 等价于 D = D command S
  • 汇编中对于控制代码的运行顺序使用条件码如 CF ZF SF OF等
  • 对于整个分支跳转使用jmp指令,jmp 可以跟一个立即数,跳转的地址是jmp后一条指令的地址加上立即数,还可以通过jz,jne等指令通过条件码进行有条件的跳转。
  • 对于if else 等语句,汇编有两种方法实现,一种是比较,然后通过jmp进行跳转,另一种是通过条件码决定是否需要进行mov,这样子对于流水线来说无需猜测,加快计算的速度,但是这样的优化很保守。如果有赋值等操作就不会进行这样的操作
  • 循环的话,类似于某一个寄存器设置为一个值,然后不停的判断,在使用jmp
  • switch则是会生成一个跳转表,可以理解为一个数组,里面存放了跳转的地址,然后,通过条件,进行jmp跳转
  • 过程调用可以理解为一个过程一个栈桢,如果传入的参数过多,寄存器放不下,会存放到栈中,同时,栈中还会放入返回地址,当过程返回时可以将这个值用来恢复PC值,同时可以通过将rsp的指针减少,创造空间放入局部变量,也可以通过pushq存入需要保存的寄存器的值,对于递归过程来说,只是call自己本身,机制于其他的过程调用的机制大致相同。
  1. 所在小组:第三组

  2. 组内昵称:hy

  3. 心得体会

编译器基于目标机器的指令集和操作系统规则生成机器代码,例如常见的 Linux x64、Win x64 等。

GCC 编译器产生汇编代码输出,然后调用汇编器和链接器,生成可执行的机器代码。

汇编代码与特定机器密切相关,是与平台(x86-64)耦合的。


  • 控制结构 if、while、switch

  • 过程:如何维护运行栈来支持过程间数据(函数参数)和控制的传递,局部变量的存储(栈上);

  • 数组、结构体的实现;

  • GDB 调试

历史观点

操作系统兼容处理器,根据处理器设计操作系统。

x86 是 Intel 处理器系统的俗称,目前也代表 64 位系统 x86-64 的意思。

AMD 生产的处理器兼容 x86 与 Intel 竞争。

程序编码

gcc -Og -o p p1.c p2.c

  • -Og 表示生成原始的 C 代码的汇编代码,不使用复杂的优化(复杂的优化会使汇编代表变现);

  • -o p 编译结果输出为文件 p

从 C 源码到可执行文件:

  • C 预处理器扩展源代码,替换 #include、#define 头文件和宏;

  • 编译器产生源文件的汇编代码:p1.s、p2.s

  • 汇编器将汇编代码转化成二进制目标代码p1.o、p2.o,目标代码是机器代码的一种形式,包含所有指令的二进制表示,但是还没有填入全局值的地址

  • 链接器将目标代码和库函数(printf)的代码合并,产生最终的可执行代码文件 p,会给各种链接的函数分配地址,保证这些函数有自己的调用(call)地址。

机器级代码

机器级代码抽象:

  1. 使用指令集架构提供的指令,CPU 顺序执行指令,指令会影响 CPU、寄存器等的状态,即使能够并行,也遵循顺序执行的模型;

  2. 使用虚拟内存地址。

  • PC 寄存器

  • 整数寄存器,64 位

  • 条件码寄存器,存放最近的状态,实现 if、while 等语句;

  • 向量寄存器,存储一组数据

机器代码把内存看成一个很大、按字节寻址的数组,各种数据类型在机器代码中都是一组连续的字节表示,也不区分无符号、有符号整数,不区分指针和整数,不区分各种类型的指针。

虚拟地址 2^48 ~ 2^64 这部分高 16 位必须设置为 0。

objdump 反汇编

反汇编程序 objdump -d x.o 可以查看机器代码,而且会转成类似汇编的代码。

数据格式

Intel 规定一个字(word)等于 16 位,两个字节。因此双字为 32 位,四字为 64 位。在 64 位机器中,指针就是四字。

  • movb 表示移动半个字,1 个字节

  • movw 移动 1 个字,2 个字节

  • movl(long word) 移动 2 个字

  • movq 移动 4 个字

数据访问

刚开始 8086 CPU 中有 8 个 16 位寄存器:rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp,名字反映了不同的用途。

扩展到 IA32 架构时,增加到 32 位。

再到 x86-64 的 CPU,增加了 8 个 r8 - r15,最终有 16 个通用的 64 位寄存器,拿来存储指针和整数。

64 位可以拿一部分低字节当 16、32 位使用,所以是向下兼容的。

rsp 寄存器是栈指针(stack point)指明运行时栈的结束位置,很多栈操作都是在这个指针上加减的相对位置操作,非常灵活

有一组编程规范指导寄存器的使用,实现栈管理、函数调用(参数、返回值)、局部变量…

操作数指示符

一组如何从寄存器、内存、中读写值的规范。

  • 立即数:$5 就是一个直接的数字,数字字面量(有 $ 符号);

  • 绝对寻址:0x400001 直接写死一个内存地址,没有 $ 符号,表示拿出这个内存地址的值;

  • ra:直接写某个寄存器,代码这个寄存器中的值 R[ra](R 可以看成所有寄存器的数组,ra 是取值下标)寄存器寻址

  • (ra)M[R[ra]]:在内存中取值寄存器中存储地址的值(M 代表内存)内存寻址

最复杂的情况,其他都是这种情况的简化:

Imm(rb, ri, s) = M[Imm + R[rb] + R[ri] * s] 这个比较复杂,Imm 是立即数偏移,rb 是基址寄存器,ri 是变址寄存器,s 是比例因子,取值必须是 1、2、4、8,访问数组会用到这种形式。

最终算出来都是一个寄存器或内存地址

数据传送指令

操作数指示符就相当于数据传送指令的参数,把一个地址的数复制到另一个地址。

mov S, D = Source -> Dest

b、w、l、q 分别代表复制 8、16、32、64 个字节。

内存地址不能复制到内存地址,需要寄存器中转

movz 指令会对不够长度的源值在目的地址填充 0。

movs 系列指令会对不够长度的情况填充符号扩展

压栈、出栈

push、pop 操作栈数据结构,会修改栈指针的值,其指向内存的某个区域。

根据惯例,栈向下生长,下面是栈底push 时栈指针值会减 8,也就是上面的地址大

因为栈在内存中,也就是大数组的一部分,因此经常使用偏移栈指针的方式访问数据,例如:movq 8(%rsp), %rdx 将第二个四字从栈中复制到寄存器 %rdx。

流程控制

流程控制是一个难点,但也是提升 CPU 性能的一个重要领域,好的分支预测技术可以显著提高 CPU 执行的效率,在并行的情况下实现流水线顺序执行的抽象。

函数

函数是非常重要的抽象,在函数的基础上,我们可以使用各种编程范式和设计模式,在高层次上思考问题。

对于函数的底层实现,则是依赖栈和寄存器进行传参、调用和返回,在 call 指令的过程中压入返回地址。

所在小组:第五组

组内昵称:锦锐

心得体会

Chatper 3 Notes

When it comes to Assembly, many programers will stop and get stuck here. And starting to complain about it, like why shall we learn this kind of low-level language, since we can do almost everything by using high-level language like C++, C, Golang. To be honest, I was one of them before I forced my self to go through this obstacle. Now I have a lots of reason telling why we should study Assembly and have a better understanding of it.

Why every programmers should learn assembly ?

  • Gain a machine-level perspective view of your code . Because master the details of how it runs is a prerequisite to understanding the deeper and more fundamental concepts. we will no longer shielded from the details.
  • Understanding the optimization capabilities of the compiler and the inefficiencies in the code .
  • (from book page 195) Being able to understand assembly code and hot it relates to the original C code is a key step in understanding how computes execute programs.

Tools we use for learning Assembly

  • gcc
  • objdump
  • DEBUG ( if you are using windows system)

What to learn

  • know the difference between the machine code and its disassembled representation (p198)
  • know how to translate the assembly code to c language code . or the other way around .

Concept

  • ATT formats and Intel formats
  • Data Formats ( page 201) , where Assembly code suffix reply on . like movl、movb、movw
  • Accessing information, where from、where to go and how to place the data. formats like [ op src dest ]
  • Operand types: immediate、Register、memory.
  • Data movement instructions. How to set up (build) a stack in memory by using instructions .
  • Conditional move instructions, how to

REFERENCE

Aside

  • We can see that the growth ha been 【phenomenal】 . p192
  • They could 【fabricate】 circuits with around 64 transistors on a single chip
  • These remarkable growth rates have been the major 【driving forces】 of the computer revolution.
  • The read operation is known as pinter dereferencing.
  • Following the idivl instruction, the quotient and remainder are copied to the top two stack locations ( p217)
  • comvdiff that 【mimics】 the operation of the assembly code . (p241)
  • It can be traslated by a single strategy, generating code that contains one or more conditional branches.
  • some varibles in the C code have 【 no counterpart 】 in the machine code
  • It is important to recognize that the suffixes for these instructions denote different coditions and not different operand size.

所在小组

第五组

组内昵称

肖思成

心得体会

  • 计算机系统使用了多种不同形式的抽象,你用更加简单的抽象模型来隐藏实现的细节
  • 机器级编程中,有两种抽象特别重要:
  1. 由指令集体系统结构或指令集架构(ISA)来定义机器级程序的格式和行为,包括处理器状态、指令格式以及指令对状态的影响
  2. 机器级程序使用虚拟地址,提供的内存模型看上去是一个非常大的字节数组
  • 大多数GCC生成的汇编代码指令都有一个字符的后缀,表明操作数的大小。
  • 一个X86-64的CPU包含一组16个存储64位值的通用目的寄存器,用来存储整数数据和指针。
  • 各种不同的操作数可以被划分为三类:
  1. 立即数(immediate),用来表示常数
  2. 寄存器(register),表示某个寄存器的内容
  3. 内存应用,会根据计算出来的地址(有效地址),访问某个内存位置
  • 汇编的数据传输可以将寄存器转移到内存中,但是不可以直接从内存转到内存,只能通过寄存器进行中转。

[quote=“maiyang, post:1, topic:1064”]

所在小组

静默组

组内昵称

Bean

你的心得体会

  • 程序最终执行的是指令编码字节序列,gcc -S查看C程序编译后的汇编代码
  • 这里对字的解释好像和之前对于机器字的理解不是很相同,日常中是不是机器字使用的比较多。。
  • 简洁引用就是将该指针放在寄存器中,然后在寄存器引用中是用这个寄存器
  • 条件跳转只能是直接跳转
  • 条件传送的优势在于简洁,虽然有很多受限制的条件,更好的匹配了现代处理器的性能特性

所在小组

静默组

组内昵称

Han

心得体会

作为底层语言的汇编语言,字面来是比较难理解的,逻辑上有点绕。在工作极少遇到,印象中只在两种情况遇到过:

  • Linux Kernel & Kernel Module
  • 分析Core Dump(无代码第三方库,需要用objdump来分析程序行为)
> objdump -d a.out

记住一些基本概念,会更容易懂:

  • %rsp 栈指针,指明运行时栈的结束位置,代码中会经常涉及栈的操作,rsp出现的频率很高
  • 另外15个寄存器使用更灵活
  • mov指令操作参数,第一个是源操作数,第二个是目的操作数.

c语言里面的指针其实是内存地址,地址被存放在一个寄存器中,获取该内存地址的值就是(%rdi),而局部变量通常会被直接保存在某个寄存器中,而不是内存中。

栈指针的记住第一章中的从高地址向低地址增长,所以

  • push的时候,%rsp是减操作
  • pop的时候,%rsp是加操作

基于条件数据传送的代码(速度更快),但与通用程序流程不同,这样编写的代码可读性会降低,很多时候编译器会采用一定的逻辑来帮助程序使用条件数据传送机制来提高执行效率,印象中intel有一套编译器,对于大型的c/c++项目,使用intel编译器生成的二进制文件,性能可能会更好。

Intel C++ compiler
Compiler comparison

所在小组

第七组

组内昵称

高华

你的心得体会

  1. 寻址方式需要注意 Imm(r_b,r_i,s) 中,s 必须是 1,2,4 或者 8
  2. objdump,gcc -Og: g 表示调试
  3. 能看懂C语言代码对应的汇编代码,可以帮助理解C语言中的概念,比如指针
    C code
    long exchange(long *xp, long y)
    {
    long x = *xp;
    *xp = y;
    return x;
    }
    Assembly code
    # xp in %rdi, y in %rsi
    exchange:
    movq (%rdi), %rax # Get x at xp. Set as return value
    movq $rsi, (%rdi) # Store y at xp.
    ret # Return
  4. leaq 指令只计算地址,且目的操作数必须是寄存器
  5. single-bit condition code register: CF,ZF,SF,OF
  6. 跳转指令
    • 直接:jmp Label
    • 间接:jmp *Operand
    • 条件:je,js …

所在小组

第三组

组内昵称

wyhqaq

心得体会

程序编码

​ gcc -O 生成不同级别的编译优化,通常-Og适合阅读,程序性能角度上-O1,-O2比较合适

​ x86-64机器代码有一些读程序员不可见的状态寄存器

  • 程序计数器(pc,%rip)
  • 16个通用寄存器
  • 条件码寄存器
  • 一组向量寄存器

数据格式

​ GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。例如movb(传送字节)、movw(传送字)、movl(传送双字)、movq(传送四字)

访问信息

​ %rax 返回值

​ %rbx %rbp %r12-%r15 被调用者保存

​ %rdi %rsi %rdx %rcx %r8 %r9 第1-6个参数

​ %rsp 栈指针

​ %r10 %r11调用者保存

​ 程序栈向下增长,栈顶元素地址是最低的

过程

​ 过程就是一个函数调用,涉及 传递控制、传递数据、内存管理三大块。

  • 传递控制

    • 函数P调用Q时,P先将下一条地址A压入栈中,然后将PC设为Q的起始地址。函数执行完会调用ret指令,会弹出A并将PC设为A
  • 数据传送

    • 默认最多传送6个整型参数,当大于6个时,用新函数的栈来传递。

所在小组

第二组

组内昵称

可可

心得体会

程序的机器级表示

一本机器码的速查字典

3.1~3.7讲述了从机器码的表示到一些常用的命令(eg.各个字的表示,各种常见的指令) 这些都可以作为后续的速查字典。

3.4访问信息,几个寄存器在这里列出

寄存器除了之前说的所谓cache之外,它还承担着其他重要的职责,一些约定俗成的职责。比如传参,第一个参数

就di,返回的值就是ax。这也就解释了C中我们只能最多返回一个值。包括还有常说的callee-savedcaller-saved也影响到了我们汇编代码的实现。个人认为这块的理解关系到我们对代码的理解。

3.5记不住的运算符(ps.毕竟很少用到)。

【重要】3.7过程(procedures)–回想起了数个面试的场景。我们程序的数据究竟是怎样的组织。

参数传递

上面说到了di,si这些寄存器,对于大于6个的 那么就需要调用方的栈帧里包含这些参数空间。


发散之go

  1. go自己实现了栈
  2. 参数传递依赖于栈(存疑?待求证)
  3. 参数返回也是保存在栈帧。(这也就是go可以有多个返回值的原因)

所在小组

第二组

组内昵称

李显良

你的心得体会

第三章 程序的机器表示

机器级代码

  1. 汇编代码

    汇编代码表示接近机器代码,与二进制的目标代码相比汇编代码更具有可读性

gcc -S code.c 可以生成汇编代码
gcc -o code.c 可以生成二进制代码
objdump -d code.o 可以将二进制代码翻译成汇编代码
  1. 汇编代码是怎么映射到二进制机器代码的
    IA32指令定义了汇编代码和机器代码的映射关系,IA32的指令长度在1-15个字节范围内,指令设计常用指令字节数少,不常用字节数多

数据格式

  1. 数据的传送指令

    movb:传送字节

    movw:传送字

    movl:传送双字

访问信息

  1. IA32 8个32位寄存器

    %esp:栈指针,指向堆栈段

    %ebp:帧指针(基地址指针)

    %eip: 存储一条要执行的指令的偏移地址

    %eax: 累加寄存器,可以用于乘、除

    %ecx: 计数寄存器,在循环和字符串操作时,控制循环次数

    %edx: 数据寄存器,用于输入/输出操作,还于AX寄存器用于涉及大值的乘法和除法运算

    %ebx: 用于索引地址

    %esi: 用于字符串操作源的索引

    %edi: 用作字符串操作的目标索引

调用过程

  1. CALL
    CALL指令调用另外一个函数,通过RET指令将控制返回给调用过程,PUSH和POP进行堆栈操作,堆栈是朝着低地址方向增长,因为栈是从上往下的

  2. 局部变量内存的管理
    数据的传递、局部变量的分配释放都是通过操作程序栈来实现的

所在小组

第二组

组内昵称

Joey

你的心得体会

  1. 计算机对有符号数和无符号数一视同仁,得益于有符号数的精妙设计,只需要coder自己判断CF和OF标志位的值的组合就可以完成所有判断,见p137

  2. 使用条件传送而非条件跳转指令节省分支预测的成本(少分支的意义在于填充满流水线,而使用条件传送指令的话下一条指令就不需要预测了,直接可以充满流水线),预测错误的代价是很大的(15~30个时钟周期),见p145

  3. 由于寄存器够用,所以简单的函数的局部变量可以直接用寄存器来存,而不需要使用内存,再加载的寄存器里进行运算,这就是xor-based swap不如temp-based swap的原因

  4. 在函数调用规约里,一般rax用来存返回值,所以在逆向C代码的时候,要根据上下文猜测变量对应哪个寄存器,或者是对应哪个内存地址

所在小组

第五组

组内昵称

孙恒

你的心得体会

寄存器:

  • 程序计数器 PC 给出将要执行的小一条指令在内存中的地址
  • 整数寄存器文件 存储地址或整数数据,保存临时数据
  • 条件码寄存器 最近执行的算术或逻辑指令的状态信息,用来实现控制或数据流中的条件变化,比如if while等
  • 一组 向量寄存器 存放一个或多个整数或浮点数

机器代码只是简单的将内存看成一个很大的、按字节寻址的数组。汇编代码不区分有符号或无符号整数,不区分各种类型的指针

程序内存包含: 程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,用户分配的内存块(比如说用malloc库函数分配的)。

code 意义
%rip PC 程序计数器,
push 压入程序栈
pushq 将四字(quad words)压入栈
movb movw movl movq S,D D<-S 传送字节 传送字 传送双字 传送四字
movzbl movzbw movzlq movzwq S, R R<-(零扩展)S 做零扩展后传送置字节,字,双字,四字
movsbl movsbw movsbl movsbq S, R R<-(符号扩展)S 做符号扩展后传送到字节, 字 双字 四字
leaq S,D D<-&S 加载有效地址(load effective address) 将有效地址写入到目的操作数[即生成指针],还可以进行普通的算术操作
cltq 把%eax符号扩展到%rax
CMP S1, S2 比较。 根据两个操作数之差来设置条件码,与SUB指令的行为是一样的,但是不会9修改dest里的值,只会设置四个condition flags
TEST S1, S2 测试. TEST指令的行为与AND指令一样
test %rax, %rax 用来检测%rax是零、负数、还是正数,至于是>= 0还是==0还是<=0,依据值类型而定

必须记忆的

register 作用
%rax 返回值,Temporary data
%rbx 被调用者保护
%rbp 被调用者保存
%rsp 栈指针Location of runtime stack, 用来指明 run-time栈的结束位置 , used to indicate the end position in the run-time stack
%rip 程序计数器/指令指针instruction point, 当前正在执行指令的地址location of current code control point.

l或b结尾的是8位,x/i结尾的是16位,e开头的是32位,r开头的是64位

operand Specifiers /操作数指示符

  • 立即数寻址 $Imm在ATT格式的汇编代码中,书写方式是**后面跟一个整数(c标准表示的)** 如`-577 或 $0x1F`
  • 寄存器寻址 ra符号ra 表示任意寄存器a
  • 存取器寻址Mb[Addr] 表示 对存储在内存中从地址Addr开始的b个字节值的引用
  • 间接寻址 (ra )

压入和弹出栈数据

栈可以实现为一个数组,在x86-64中,程序栈存放在内存中某个区域。栈向下增长,向低地址方向增长, 栈顶元素的地址是所有栈中元素地址中最低的,%rsp 保存着栈顶元素的地址

pushq %rbp的行为等价于下面两条指令

subq $8, %rsp      Decrement stack point 
movq %rbp, (%rsp)  Store %rbp on stack 

shift Operations:

  1. 移位量是一个立即数或者放在单字节寄存器%cl中
  2. 移位操作对w位长的数据值进行操作,移位量是由%cl寄存器的低m位决定的,这里2m=w,高位会被忽略

特殊的算术运算

P134 对于大多数的64位除法应用来说,除数也常常是是一个64位的值,这个值应该存放在%rax, %rdx的位应该设置为全0(无符号运算)或%rax的符号位(有符号运算), 有符号运算的操作可以用指令cqto来完成。这条指令不需要操作数—–它隐含读出%rax的符号位,并将它复制到%rdx的所有位

Condition Code

描述了最近的算术或逻辑操作的属性,可以检查这些寄存器来执行条件分支指令

循环和switch

  • 跳转表通过数组或者平衡二叉树实现,
  • case是负数或case从极大值开始会增加bias偏置量,通过加或减去bias让第一个case等于0,以免出现负数索引
  • case具有大跨度且数量较少[即相对稀疏]的情况会转换为if—elseif-else树

一个switch语句是否总是比一系列的if-else语句高效的多?

答: 先来说清楚switch的基本概念。

  • 可以根据一个整数索引值进行多重分支(multiway branching)
  • 使用跳转表(jump table),优点执行switch case语句的时间和switch case的数量无关
  • gcc根据case的数量和switch的稀疏程度来翻译switch语句
  • switch的case较多(4 or more ) 并且值的范围跨度比较小时,就会使用跳转表

过程

当过程P调用过程Q时,P的代码必须首先把参数复制到适当的寄存器中。P168

通过寄存器最多传递6个整型(整数和指针)参数

超过6个部分通过栈来传递,通过栈传递参数时,所有的数据大小都向8的倍数对齐

使用地址运算符&,调用代码必须分配一个栈帧stack frame

寄存器%rbx,%rbp和%r12~%r15 被划分为被调用者保存寄存器callee saved register。过程P调用过程Q时,Q必须保存这些寄存器的值。要么根本不去改变它,要不push %rbx 到栈中返回前pop %rbx

Callee-saved registers (AKA non-volatile registers, or call-preserved) are used to hold long-lived values that should be preserved across calls.

其他的寄存器(除了栈指针%rsp)被划分为 caller saved 调用者保存寄存器,这就意味着任何函数都能修改它们, 在调用之前是否保存这个数据是调用者(父函数)的责任

Caller-saved registers (AKA volatile registers, or call-clobbered) are used to hold temporary quantities that need not be preserved across calls.

每次函数调用都有它自己私有的状态信息(保存的返回值和被调用者保存寄存器的值)存储空间,如果需要,它还可以提供局部变量的存储[即栈空间],

所在小组

第六组

组内昵称

之昂

心得体会

  • 计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理内存、读写存储设备上的数据,以及利用网络通信。
    GCC c语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编器和链接器,根据汇编代码生成可执行的机器代码。最大的优点用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的。
  • 程序编码
    假设一个C程序,有两个文件p1.c和p2.c。我们用Unix命令行编译这些代码:
    linux> gcc -0g -o P p1.c p2.c 代码
  • 程序内存
    包括程序的可执行机器代码,操作系统需要的一些信息,管理过程调用和返回的运行时栈,用户分配的内存块。==程序内存是用虚拟地址来寻址==
  • 压入和弹出栈数据
    栈是一种数据结构,可以添加或者删除值,不过要遵循“后进先出”的原则。通过push操作把数据压人栈中,通过pop操作删除数据;它具有一个属性:弹出的值永远是最近被压人而且仍然在栈中的值。栈可以实现为一个数组,总是从数组的一端插入 和删除元素。这一端被称为栈顶。
    pushq指令的功能是把数据压人到栈上,而popq指令是弹出数据。这些指令都只有一个操作数–压人的数据源和弹出的数据目的。
  • 加载有效地址
    加载有效地址(load effective address)指令leaq 实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。
  • 递归过程
    递归调用一个函数本身与调用其他函数是一样的。 栈规则提供了–种机制,每次函数调用都有它自已私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间。如果需要,它还可以提供局部变量的存储。栈分配和释放的规则很自然地就与函数调用-返回的顺序匹配。这种实现函数调用和返回的方法甚至对更复杂的情况也适用,包括相互递归调用(例如,过程p调用Q,Q再调用P)。

所在小组

 第2组

组内昵称

心得体会

1.机器级代码

指的是利用计算机中多种不同形式的抽象模型实现程序细节,比较重要的两种抽象:
指令集:用来定义程序的格式和行为
虚拟内存:机器级别程序使用的内存地址是虚拟地址

2.栈

基本原则:后进先出
基本操作:`push` 和 `pop`来操作数据,同时会修改栈指针值,并指向最新的内存地址
内存地址:从上到下依次变小	 

3.条件码

常用条件码:
    CF:进位标志
    ZF:零标志
    SF:符合标志
    OF:溢出标志
使用方式:
    1.组合条件码,将一个字节设置成 0 or 1
    2.跳转到程序的指定部分
    3.有条件的传送数据

所在小组

一组

组内昵称

nigel

你的心得体会

1.如何由机器代码生成汇编代码?

objdump -d再加上文件名即可直接在终端看到由反汇编器恢复的汇编代码。注意,文件名并不一定得是.o文件,任何可执行文件都可以。

结果如下:

  1. 32位和64位的基本数据类型大小对比:

32位:

char:1字节,char*:4字节,short int:2字节,int:4字节,unsigned int:4字节,float:4字节,double:8字节,long:4字节,long long:8字节,unsigned long:4字节

64位:

char:1字节,char*:8字节,short int:2字节,int:4字节,unsigned int:4字节,float:4字节,double:8字节,long:8字节,long long:8字节,unsigned long:8字节

3.几个术语:

字节:8位,后缀:b

字:16位,后缀:w

双字:32位,后缀:l

四字:64位,后缀:q

MOV类指令也有movb,movw,movl,movq,分别对应这几种大小的操作数。

寻址方式:

1: $+立即数,则取得的操作数就是立即数

2:立即数,则取得的操作数就是以立即数为地址,对应取出的操作数

3:寄存器,则取得的操作数就是以寄存器的值

4:(寄存器),则取得的操作数就是以寄存器的值为地址,对应取出的操作数

5:立即数1(寄存器1,寄存器2,立即数2),则取得的操作数就是以立即数1的值+寄存器1的值+寄存器2的值*立即数2为地址,对应取出的操作数

6.有了最通用的第5条,其他变种都能写出,比如(,寄存器2,立即数2),则取得的操作数就是以寄存器2的值*立即数2为地址,对应取出的操作数

mov指令

1.mov指令的顺序是从左到右,如mov a,b,则把a的值复制给b

2.除了之前提到的movb,movw,movl,movq,还有movabsq,代表传送绝对的四字,movq虽可传四字,但一旦要传立即数,则只能传32位补码表示的立即数,随后把它符号拓展到64位。而movabsq可以直接传64位的立即数,但是它只能以寄存器作为目的地。

3.所有mov指令都不支持从一个内存地址直接传到另一个内存地址,如movw (%rax),4(%rsp)是不行的。

4.决定mov使用哪个后缀的是寄存器的大小,当两边操作的都是寄存器时,若大小不同,必须用第5条中的小数据复制到大目的地的类型的mov指令,当两边操作的是立即数和内存时,可以以立即数大小为准,

例子:movl $0x4050,%eax 0x4050虽然是2字节,但%eax是4字节,所以movl

 movw %bp,%sp

 movb (%rdi,%rcx),%al

 movb $17,(%rsp)  立即数->内存

 movq %rax,-12(%rbp)

5.当想将小的数据复制到大的目的地时,可以用movz或movs,前者代表用0填充高字节,后者代表用符号填充高字节,后面还要加上两种转换数据的大小,

比如movzbw(字节->字,0填充),movswq(字->四字,符号填充),还有一种cltq指令,特指%eax->%rax的符号拓展转换,等价于movslq %eax,%rax

注意movs和movz都是以寄存器为目的地的。

根据以上信息,可以知道,之前的第4点中的特殊规则其实相当于是说movl可以实现movzlq的功能

  1. 所在小组: 静默组
  2. 组内昵称:frances
  3. 心得体会

第三章主要是从汇编代码级来了解程序是如何运行,通过阅读汇编代码一方面可以理解编译器的优化能力帮助分析优化源码,另一方面可以了解程序存储运行控制信息的细节可以用来防御代码漏洞等。有一定的汇编基础,所以这章比上章读起来稍微轻松一些,长假期间闲时顺手翻完。

主要记录了以下几个知识点:

  • 首先是定义
    a. PC,存储将要执行的下一条指令在内存中的地址
    b. 整数寄存器,存储整数数据和指针,有的jicunq被用来记录某些重要的程序状态
    c. 条件码,保存最近执行的算术或逻辑指令的状态信息,用来实现控制或数据流中的条件变化
    d. 向量寄存器用来存放一个或多个整数或浮点数值。
    e. 一个X86-64的CPU中包含一组16个64位通用目的寄存器,用来存储整数数据和指针。其中%rsp是栈指针用来指明运行时栈的结束位置。

  • 数据传输指令、入栈出栈数据指令、算术逻辑操作以及跳转指令之前都有一定的了解,这次补充了下乘法和除法的运算逻辑。

    • 2个64位有符号/无符号整数相乘需要128位来表示,X86-64对128位数的操作提供有限的支持。mulq(无符号数乘法)和imulq(补码乘法)都要求一个参数必须在寄存器%rax中,另一个作为指令的源操作数给出,乘积存放在寄存器%rdx(高64位)和%rax(低64位)中。
    • 除法或取模运算,由单操作数除法指令来提供,idivl将寄存器%rdx(高64位)和%rax(低64位)中的128位数作为被除数,除数作为指令操作数给出,将商存储在%rax中,余数存储在%rdx中。
  • 条件码通常不会直接读取,常用的三种方法:I)根据条件码的某种组合将一个字节设置为0/1; II)条件跳转到程序的其他部分;III)有条件地传送数据。 常用的条件码寄存器:

    • CF:进位标志,可用来检查无符号操作的溢出
    • ZF:零标志位
    • SF:符号标志
    • OF:溢出标志,最近的操作导致一个补码溢出
  • 基于条件数据传送的代码会比基于条件控制转移的代码性能要好是因为:首先,现代处理器通过pipelining重叠连续指令的步骤来获取高性能。

    • 当机器遇到条件跳转指令时只有当分支条件求值完成后才能决定分支往哪走,分支预测逻辑可靠的情况下流水线中依旧会充满指令并行,但是当跳转错误预测时需要丢掉它为跳转指令后续所有指令已做的工作,浪费大约15-30个时钟周期,导致程序性能严重下降。
    • 同条件跳转不同,处理器不需要预测结果就可以执行条件传送,处理器只是读源值,检查条件码,然后要么更新目的寄存器要么保持不变。
    • 不过条件传送也不总是会提高代码效率,需要权衡复杂计算开销和分支预测错误开销来判断具体使用哪种。
  • 过程是软件中的一种重要抽象,要提供对过程的机器级支持需要处理传递控制、传递数据、分配和释放内存等机制。C语言过程调用机制的一个关键特性在于使用了栈数据结构的后进先出的内存管理原则,每个过程在栈中都有自己的私有空间,多个未完成调用的局部变量不会相互影响。

  • C语言对数据引用不进行任何边界检查,局部变量和状态信息都存放在栈中,这两种情况结合在一起,对越界的数组元素写操作会破坏存储在栈中的状态信息,即常见的缓冲区溢出问题。 这种问题可以被用来攻击系统安全,将攻击代码包含在输入字符串中或者将一个指向攻击代码地址的指针覆盖返回地址等。现代的编译器和操作系统实现了很多机制限制入侵者通过缓冲区溢出攻击获得系统控制。常见的机制:

    • 栈随机化:为了插入攻击代码,攻击者既要插入代码又要插入指向该代码的指针。这个指针也是攻击字符串的一部分,要产生这个指针需要知道字符串存放的栈地址。栈的位置在程序每次运行时都有变化使得改地址难以预测。在Linux中栈随机化已经成为标准行为,ASLR地址空间布局随机化。
    • 栈破坏检测:第二道防线是能够检测到何时栈被破坏,栈保护者机制通过canary金丝雀值是否被改变来判断,该值是程序每次运行随机产生的,如果被某个函数或操作改变那么异常中止该程序。
    • 限制可执行代码区域:消除攻击者向系统中插入可执行代码的能力。