zoukankan      html  css  js  c++  java
  • 计算机系统导论——读书笔记——第三章 程序的机器级表示

    第三章 程序的机器级表示

    3.1 历史观点

    3.2 程序编码

    1.命令行

    (1)编译

    linux> gcc -Og -o p p1.c p2.c #编译p1.c和p2.c文件

    (2)ATT格式汇编

    linux> gcc -Og -S mytry.c #产生C语言汇编文件mytry.s,可直接查看

    linux> gcc -Og -c mytry.c #产生二进制目标代码文件mytry.o,无法直接查看

    linux> objdump -d mytry.o #输出mytry.o文件的机器代码和反汇编代码,在命令行中输出

    linux> gcc -Og -o prog main.c mytry.c #生成可执行文件prog

    linux> objdump -d prog #输出prog文件的机器代码和反汇编代码,在命令行中输出

    (3)Intel格式汇编

    linux> gcc -Og -S -masm=intel mytry.c #产生C语言汇编文件mytry.s,可直接查看

    2. gcc命令的过程

    C预处理器(扩展源代码,插入#include的文件并扩展#define定义的宏)

    编译器(产生源文件的汇编代码.s)

    汇编器(将汇编代码转换成二进制目标代码文件.o)

    链接器(将目标代码文件与实现函数库的代码合并,并产生可执行文件.p)

     

    3.2.1 机器级代码

    1.两种抽象

    (1)指令集架构/指令集体系结构(Instruction Set Architecture, ISA)

    (2)虚拟地址

     

    3.2.2 代码示例 

    1.汇编vs反汇编

    (1)每一条指令的长度:1-15字节;越常用的指令对应的二进制代码越短

    (2)反汇编器省略许多指令结尾的q,而给call和ret指令添加q后缀

    2.反汇编.o vs 反汇编prog

    (1)地址不同

    (2)反汇编prog填上了callq等指令所需的地址

    (3)反汇编prog在末尾插入nop,使函数代码变为16字节,便于储存下一个代码块

     

    3.2.3 关于格式的注释

    1."."开头的行:伪指令,指导汇编器和链接器工作

    2.ATT格式汇编vsIntel格式汇编

    (1)intel省略表示大小的后缀

    (2)intel省略寄存器前%

    (3)intel用不同的方式描述内存中的位置,如“QWORD PTR [rbx]”替代“(%rbx)”

    (4)在带有多个操作数的指令情况下,列出的操作数顺序相反

     

    3.3 数据格式

    1.C语言数据类型在x86-64中的大小

    注:

    (1)b = byte = 8 bits, w = word = 16 bits, l = long word = 32 bits, q = quad word = 64 bits

    (2)浮点数:s = short float(?我猜的) = 32 bits, l = long float(?) = 64 bits

    (3)l既表示4字节整数又表示8字节浮点数,但并不会产生歧义,因为浮点数使用一组完全不同的寄存器

     

    3.4 访问信息

    1.16个整数寄存器(非常重要)

    注:

    (1)生成小于8字节结果的指令,剩下的字节会如何?

    生成1或2字节的指令保持剩余字节不变,生成4字节的指令把高位4字节置0

    (2)(i guess)栈指针 %rsp = register stack pointer

     

    3.4.1 操作数指示符

    1.操作数(operand)

    (1)立即数(immediate):表示常数值,ATT汇编代码中表示为“$”+标准C表示法表示的整数,自动选择最紧凑的方法编码(?)

    (2)寄存器(register):表示某个寄存器的内容,16个寄存器中的低位1、2、4、8字节中的一个作为操作数

    (3)内存引用:根据有效地址访问内存位置

    注:

    (1)ra表示任意寄存器a,R[ra]表示它的值(这是将寄存器集合视为数组R,寄存器标识符作为索引)(R = register)

    (2)Mb[Addr]表示对存储在内存中地址Addr开始的b个字节值的引用,可省去下标b(M = memory)

    (3)Imm(rb, ri, s)是最常用的内存引用的寻址模式,包含:立即数偏移Imm(缺省为0)、基址寄存器rb(缺省为0)、变址寄存器ri(缺省为0)、比例因子s(s=1,2,4,8,缺省为1),有效地址为Imm+R[rb]+R[ri]*s

     

     

    写在前面:书中把许多不同的指令划分为指令类,每一类执行相同的操作,只不过操作数的大小不同

    3.4.2 数据传送指令

    1.MOV类——简单的数据传送指令:把数据从源位置复制到目的位置

    格式:MOV source源操作数, destination目的操作数

    注:

    (1)寄存器部分的大小必须与指令做后一个字符(b、w、l、q)指定的大小相匹配

    (2)S、D均可以是内存地址或寄存器,但不能同时为内存地址;从内存传送数据到内存需要两条指令:内存->寄存器,寄存器->内存

    (3)movq vs movabsq: movq只能表示以表示为32位补码数字的立即数作为源操作数,然后扩展符号得到64位,而movabsq能够以任意64位立即数作为源操作数,但只能以寄存器作为目的

     

    2.MOVZ和MOVS类:将较小的源值复制到较大的目的时使用

    格式:

    (1)零扩展(MOV zero)——高位补0: MOVZ+源大小+目的大小 source, register

    (2)符号扩展(MOV sign)——高位扩展符号位:MOVS+源大小+目的大小 source, register

    注:

    (1)S可以是内存地址或寄存器,R只能是寄存器

    (2)不存在movzlq指令,但可以用以32位寄存器为目的的movl指令实现,高位4字节置0

    (3)cltq指令无操作数,效果与movslq %eax,%rax完全一致,但是编码更紧凑(我理解为:更省地方)

    (4)一个有趣的小练习

    答案:

     

     

    3.4.3 数据传送示例

    1.示例

    2.一个有趣且有难度的小练习

    答案

    注意:

    (1)对于长度扩展的情况:先扩展,再转移

    (2)对于长度缩减的情况:先转移,再截断

    (3)在有符号数扩展时,用movs;无符号数扩展时,用movz

    (4)movzbl与movzbq等价,因为前者会自动把高位4字节补0,但为了优化CPU的效率,选择了前者(问了助教学长,但是仍存疑)

     

    3.4.4 压入和弹出栈数据

    1.栈和栈指针%rsp

     

    2.pushq和popq指令:

    pushq:栈指针%rsp减8, 在栈顶写入新值

    popq:保存栈顶元素,栈指针%rsp加8

    pushq %rbp等价于:

      subq $8,%rsp

      movq %rbp,(%rsp)

    popq %rax等价于:

      movq (%rsp),%rax

      addq $8,%rsp

      

    3.5 算数和逻辑操作

    算数和逻辑操作类一览:加载有效地址、一元操作、二元操作、移位

    注:

    (1)除了加载有效地址leaq之外,每一种指令类都包含针对b、w、l、d四种不同大小数据类型的指令

     

    3.5.1 加载有效地址

    1.加载有效地址(load effective address = lea)leaq指令是movq指令的变形:将有效地址写入目的操作数(必须是一个寄存器),既可以为后续内存引用产生指针,也可以做完全与计算有效地址无关的灵活操作。

    2.一些简单的小栗子

     

    3.5.2 一元和二元操作

    1.一元操作:只有一个操作数,既是源也是目的,可以是内存地址或寄存器

    2.二元操作:第一个操作数是源,可以是立即数、内存地址、寄存器;第二个操作数既是源也是目的,可以是内存地址或寄存器

    3.一个厉害的小练习(一定要做做)

    答案

     

    3.5.3 移位操作

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

    例如,当寄存器%cl的十六进制值是0xFF时,指令salb会移7位,指令salw会移15位,指令sall会移31位,指令salq会移63位。

    2.第二个操作数:目的操作数,可以是一个寄存器或一个内存位置

    3.左移:SAL和SHL完全相同

    4.右移:SAR是算术右移(shift arithmetically - right),左侧补充符号位,SHR是逻辑右移,左侧补0

     

     

    3.5.4 讨论

    1.一个巧妙的小栗子

     

    3.5.5 特殊的算数操作

    1.特殊的算数操作指令

    注:

    (1)八字-16字节-oct word

    (2)imulq既可以是双操作数指令(见3.5.2),也可以是单操作数指令,汇编器根据操作数数目判断指令

    (3)R[%rdx]:R[%rax]表示一个128位的八字,高64位存放在%rdx中,低64位存放在%rax中

    (4)idivq和divq把商储存在%rax中,把余数储存在%rdx中

    2.#include <inttype.h>中定义了128位整数__int128和unsigned __int128

    3.乘法示例

    4.除法示例

    5.一个有难度的课后习题

    我的答案(我的汇编代码注释格式可能不太规范,思路应该没什么问题,凑合看看叭) 

     

     

    3.6 控制

    1.机器代码实现有条件的行为的两种基本低级机制:测试数据值,然后根据测试结果改变控制流(即当条件满足时,程序沿着一条路径执行,当条件不满足时,程序沿着另一条路径执行,见3.6.5用条件控制来实现条件分支)或改变数据流(即计算出一个条件操作的两种结果,然后根据是否满足条件从中选取一个,见3.6.6用条件传送来实现条件分支)。

    在两条路径/两个表达式都很容易计算时,后者性能更优,原因在于现代处理器使用流水线(pipeline)来获得高性能,前者的分支预测错误处罚较高,详见第4章、第5章;而在一些情况下,前者性能更优,因为后者会引发一些诸如引用空指针之类的错误。

     

    3.6.1 条件码

    1.条件码寄存器(condition code register):除了整数寄存器,CPU还维护着一组单个位的条件码寄存器,描述最近的算术或逻辑操作的属性。

    (1)CF:进位标志carry flag.最近的操作使最高位产生进位,可用于检查无符号操作的溢出.

    (2)ZF:零标志zero flag.最近的操作得出的结果为0.

    (3)SF:符号标志sign flag.最近的操作得到的结果为负数.

    (4)OF:溢出标志overflow flag.最近的操作导致一个补码溢出——正溢出或者负溢出.

    2.改变条件码的指令

    (1)leaq不改变条件码

    (2)图3-10中除了leaq以外的指令都会设置条件码:

      a.逻辑操作:CF、OF置为0

      b.移位操作:SF置为最后一个被移出的位,OF置为0

      c.INC和DEC:设置OF和ZF,不改变CF

    (3)CMP类和TEST类:只改变条件码,不更新目的寄存器的指令

     

    3.6.2 访问条件码

    1.条件码的使用方法:

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

    (2)条件跳转到程序的某个其他的部分

    (3)有条件地传送数据

    2.SET指令类:根据条件码的某种组合将一个字节设置为0或1

    注:

    (1)SET指令类的指令后缀表示不同的条件,而不是数据大小 

    (2)目的操作数是一个低位单字节寄存器元素,或一个字节的内存位置,指令会将这个字节设置成0或1;为得到32位或64位结果,需要对高位清零

    (3)溢出符号OF=0:无溢出;

      溢出符号OF=1,有溢出,SF=0表示此时负溢出(实际结果<0),SF=1表示正溢出(实际结果>0)

     

    6.3.3 跳转指令

    1.跳转指令

    注:

    (1)直接跳转:给出一个标号作为跳转目标,并将跳转目标作为指令的一部分编码,例如“jmp .L1”

    (2)间接跳转:跳转目标是从寄存器或内存位置中读出的,写法为‘*’加一个操作数指示符;例如:jmp *%rax表示以%rax中的值作为跳转目标,jmp *(%rax)表示以%rax中的值作为读地址,从内存中读出跳转目标

     

    3.6.4 跳转指令的编码

    1.跳转指令的编码方式:

    (1)PC相对跳转(PC-relative):用目标指令的地址与紧跟在跳转指令后面那条指令的地址(即,运行到跳转指令时,程序计数器所指向的指令的地址)之差作为编码

    (2)给出“绝对”地址

    2.指令rep和repz:AMD建议编译器编写者使用rep后面跟ret的指令来避免使ret指令成为条件跳转指令的目标;否则,当分支不跳转时,jg指令会继续到ret指令,而当ret指令通过通过跳转指令到达时,处理器不能正确预测ret指令的目的。

    3.一个小练习-复习小端法&大端法

    答案

    复习:小端法&大端法

     

    3.6.5 用条件控制来实现条件分支

    1.用条件控制来实现条件分支:测试数据值,然后根据测试结果改变控制流(即当条件满足时,程序沿着一条路径执行,当条件不满足时,程序沿着另一条路径执行)

    2.if-else语句的汇编实现

    1 if(test-expr)
    2     then-statement
    3 else
    4     else-statement
    if-else语句模板
    1     t = test-expr
    2     if(!t)
    3         goto false;
    4     then-statement
    5     goto done;
    6 false:
    7     else-statement
    8 done:
    汇编实现的控制流(用C语言来描述)
    1     t = test-expr
    2     if(t)
    3         goto true;
    4     else-statement
    5     goto done;
    6 true:
    7     then-statement
    8 done:
    另一种可能的翻译

    第二种翻译劣于第一种翻译,原因在于当遇到常见的没有else语句的情况时,第一种翻译可以简单地改写为:

    1     t = test-expr
    2     if(!t)
    3         goto done;
    4     then statement
    5 done:
    简单地改写

     

    3.6.6 用条件传送来实现条件分支

    1.用条件传送来实现条件分支:测试数据值,然后根据测试结果改变数据流(即计算出一个条件操作的两种结果,然后根据是否满足条件从中选取一个)

    2.条件传送指令(cmovXX,可以理解为cmovXX = compare + move + XX比较条件):如果满足XX条件,则将S值copy到D.

    注:

    (1)S是寄存器或内存地址,R只能是寄存器

    (2)只支持16、32、64位值的传送,不支持单字节传送

    (3)对所有的操作数长度都可以使用同一个指令名称,不用加后缀,因为汇编器可以从目标寄存器的名称判断操作数的长度

    3.确定分支预测错误的处罚

     

    3.6.7 循环

    1.do-while循环

    (1)通用形式

    1 do
    2     body-statement
    3     while(test-expr);

    (2)翻译方式

    1 loop:
    2      body-statement
    3      t = test-expr
    4      if(t)
    5          goto loop;

    2.while循环

    (1)通用形式

    1 while(test-expr)
    2     body-statement

    (2)翻译方式

    a.跳转到中间(jump to middle)

    1     goto test;
    2 loop:
    3     body-statement
    4 test:
    5     t = test-expr;
    6     if(t)
    7         goto loop;

    b.guarded-do

    1 t = test-expr
    2 if(!t)
    3     goto done;
    4 loop:
    5     body-statement
    6     t = test-expr
    7     if(t)
    8         goto loop;
    9 done:

    3.for循环

    (1)通用形式

    1 for(ini-expr; test-expr; update-expr)
    2     body-statement

    等价while

    1 ini-expr;
    2 while(test-expr){
    3     body-statement;
    4     update-expr;
    5 }

    (2)翻译方式

    a.跳转到中间(jump to middle)

    1 ini-expr;
    2 goto test;
    3 loop:
    4     body-statement
    5     update-expr;
    6 test:
    7     t = test-expr;
    8     if(t)
    9         goto loop;

    b.guarded-do

     1 ini-expr;
     2 t = test-expr;
     3 if(!t)
     4     goto done;
     5 loop:
     6     body-statement
     7     update-expr;
     8 test:
     9     t = test-expr;
    10     if(t)
    11         goto loop;
    12 done:

     

    3.6.8 switch语句

    1.跳转表(jump table):

    (1)是一个数组,表项i是一个代码段的地址;

    (2)使用跳转表执行开关语句的时间与开关情况的数量无关,因而高效;

    (3)当开关比较多,且值的跨度范围比较小时,GCC就会使用跳转表。

    2.跳转表的使用

    step 1: 开关变量n--(加减常数c)-->索引值index(范围0~常数a);

    step 2: 比较索引值index与常数a,如果index>a跳转到默认代码处;

    step 3: 跳转到*jt[index]处,汇编语句:jmp *.L4(,%rsi,8),这是一个间接跳转(假设索引值index储存于%rsi,跳转表起始位置为.L4).

    3.跳转表的汇编声明

     1     .section          .rodata
     2     .align 8               Align address to multiple of 8
     3 .L4:    
     4     .quad .L3            Case 100: loc_A
     5     .quad .L8            Case 101: loc_def
     6     .quad .L5            Case 102: loc_B
     7     .quad .L6            Case 103: loc_C
     8     .quad .L7            Case 104: loc_D
     9     .quad .L8            Case 105: loc_def
    10     .quad .L7            Case 106: loc_D           

    这些声明表明,在叫做“.rodata”(只读数据,Read-Only Data)的目标代码文件的段中,应该有一组7个“四”字(8个字节),每个字的值都是与指定的汇编代码标号(例如.L3)相关联的指令地址。标号.L4标记出这个分配地址的开始。

    4.一个有点儿难的小练习

    答案

     

     

     

    3.7 过程

    过程 = 传递控制 + 传递参数 + 分配和释放内存

     

    3.7.1 运行时栈

    1.栈帧(stack frame): 当过程所需内存超过寄存器能存放的大小,就会在栈上分配空间。

    通过寄存器,调用函数P最多可以传递6个参数给被调用函数Q,其余参数需要提前存放在P的栈帧中。

    返回地址指明当Q返回时,要从P的哪个位置继续执行

     

    3.7.2 转移控制

    1.转移控制指令

    call Q指令:把调用函数的返回地址A压入栈,并将PC设置为被调用函数的起始地址 

    ret指令:从栈中弹出返回地址A,并将PC设置为A

    目标(直接/间接):指明被调用函数的起始地址

    2.程序计数器PC存放于%rip中

    其他的看书叭~

     

    3.7.3 数据传送

    1.通过寄存器传递参数:最多通过寄存器传递6个整型/指针参数,顺序固定

    2.通过栈传递参数:多余的参数7~n要放到调用者的栈帧中,参数7在栈顶;所有数据大小向8的倍数对齐

     

    3.7.4 栈上的局部存储

    1.局部数据必须放在内存中的常见情况:

    (1)寄存器不足够存放所有的本地数据

    (2)对一个局部变量使用运算符‘&’,因此必须能够为它产生一个地址

    (3)某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到

     

    3.7.5 寄存器中的局部存储空间

    1.被调用者保存寄存器(%rbx,%rbp,%r12,%r13,%r14,%r15):被调用函数负责维护这些寄存器中的值不变;方法一:根本不改变值,方法二:原值压栈,返回前弹栈.

    2.调用者保存寄存器(除栈指针%rsp和被调用者保存寄存器外的其他所有寄存器):调用者函数要在调用其他函数前保存好这些值.

    3.7.6 递归过程

    3.8 数组分配和访问

    3.8.1 基本原则

    1.声明:T A[N]

    2.声明的效果

    (1)分配大小为L*N字节的内存位置,L为数据类型T所需字节长度。例如:int A[10]分配4*10字节的内存

    (2)引入标识符A作为指向数组开头的指针,存放地址xA。例如:A是int*类型的指针

    3.数组访问:

    (1)地址访问:&A[i] = xA + L*i

    (2)元素访问:movl (%rdx,%rcx,4),%eax

               %rdx存放地址xA,%rcx存放索引值i,4是伸缩因子(值可以为1,2,4,8)

    3.8.2 指针运算

    1.符号*:产生指针

      符号&:间接引用指针

    2.等价的关系

    (1) Expr = *&Expr

    (2) 取元素:A[i] = *(A+i)

    (3) 取地址:A+i = &A[i]

    (4) 取长度:i = &A[i] - A (i的数据类型为long)

    3.8.3 嵌套数组

    1.两种等价的声明:

    (1) T D[R][C];

    (2) typedef T row[C];//数据类型row被定义一个长为C的数组

        row D[R];//数组D的每一个元素都是一个长为C的数组

    2.数组地址:&A[i][j] = xA + L*(C*i + j)

     

    3.8.4 定长数组

    3.8.5 变长数组

    3.9 异质的数据结构

    3.9.1 结构struct

    1.结构struct:将可能不同类型的对象聚合到一个对象中;结构的所有组成部分存放在连续的内存区域中

    2.结构的访问:

    (1)指向结构的指针是结构的第一个字节的地址;

    (2)编译器指示每个字段(field)的字节偏移,结构的地址 + 字段偏移量 = 结构内部元素的地址

    (3)结构各个字段的选取完全是在编译时处理的

    3.9.2 联合union

    1.联合union:用多种类型引用一个对象,即用不同的字段引用相同的内存块

    2.联合的总大小 = 它最大字段的大小

    3.注意字节顺序问题(一般为小端法)

    3.9.3 数据对齐

    1.对齐原则:任何K字节的基本对象的地址必须是K的倍数,K = 1,2,4,8

    2.对齐声明:.align 8表示它后面的数据的起始地址是8的倍数

    3.结构struct的对齐

    (1)结构的对齐要求Ks = max{结构中的元素的对齐要求} 

    (2)结构末尾的填充:使结构总大小为Ks的倍数,便于结构数组中每个元素的对齐

     

  • 相关阅读:
    Inno Setup区段之Dirs篇
    Inno Setup区段之Tasks篇
    leetcode刷题-69x的平方根
    7.27 判断子序列
    7.26 矩阵中的最长递增路径
    PMP | 备考笔记
    数据结构--数组存储二叉树(Java)
    数据结构--哈希表(Java)
    查找--斐波那契查找(Java)
    牛客网--字节跳动面试题--特征提取
  • 原文地址:https://www.cnblogs.com/tanshiyin-20001111/p/11619024.html
Copyright © 2011-2022 走看看