【CSAPP笔记】第三章 程序的机器级表示(1)——整数及指令

164 阅读3分钟

3.2 程序编码

【例】Linux可以用命令行将源文件编译为可执行程序

gcc -Og -o p p1.c p2.c
  • 源代码转换为可执行代码的过程
    • 源代码(p1.c和p2.c):通过编译器生成汇编代码
    • 汇编代码(p1.s和p2.s):通过汇编器转化成二进制目标代码文件
    • 目标代码(p1.o和p2.o):通过链接器将目标代码合并为可执行文件p

【注】-Og代表原始C代码整体结构的机器代码的优化等级。如果要提高程序性能,可以使用较高的优化等级-O1-O2,但是会破坏C代码的结构

3.2.2 代码示例

long mult2(long, long);

void multstore(long x, long y, long *dest) {
    long t = mult2(x, y);
    *dest = t;
}

【编译】gcc -Og -S mstore.c,生成AT&T格式的汇编代码mstore.s。去掉伪指令后如下:

// void multstore(long x, long y, long *dest)
// x in %rdi, y in %rsi, dest in %rdx
multstore:
        pushq   %rbx            // Save %rbx
        movq    %rdx, %rbx      // Copy dest to %rbx
        call    mult2           // Call mult2(x, y)
        movq    %rax, (%rbx)    // mult2的返回值是%rax,赋值到*dest中
        popq    %rbx            // Restore %rbx
        ret                     // return

【汇编】gcc -Og -c mstore.c,生成mstore.o。内部有一段代码对应目标代码

53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3

【反汇编】objdump -d mstore.o。可以看到与编译的结果是类似的。区别:省略了一些指令的后缀'q',call和ret加上了后缀'q'(其实省略掉也没问题)

0000000000000000 <multstore>:
   0:   53                      push   %rbx
   1:   48 89 d3                mov    %rdx,%rbx
   4:   e8 00 00 00 00          callq  9 <multstore+0x9>
   9:   48 89 03                mov    %rax,(%rbx)
   c:   5b                      pop    %rbx
   d:   c3                      retq

3.3 数据格式

汇编代码的指令处理,某些指令是带后缀的,如movbmovq

  • 整型:b代表字节(8bit);w代表字(16bit);l代表双字(32bit);q代表四字(64bit)
  • 浮点型:有不同的指令,s代表单精度(4字节);l代表双精度(8字节)

image-20221128080753144.png

3.4 访问信息

通用寄存器:x86-64的CPU包含16个存储64位值的通用目的寄存器

  • 寄存器的低位部分可以独立使用,兼容历史指令
  • 不同的寄存器扮演不同的角色,比如%rax默认作为返回值

image-20221128081419954.png

3.4.1 操作数

AT&T汇编指令格式:操作码 + 操作数

movq (%rdi), %rax
addq $8,     %rbx

操作数分为三种类型:

  • 立即数:表示常数。格式是$数字,比如$-577$0x1F
  • 寄存器:表示某个寄存器的值,格式是%寄存器,比如%rax
  • 内存引用:相当于指针,根据地址访问某个内存的位置
    • 表示方式:Imm(rb,ri,s)Imm(r_b, r_i, s),分为4个部分:立即数偏移ImmImm、基址寄存器rbr_b、变址寄存器rir_i、比例因子ss(必须是1、2、4、8之一)
    • 有效地址:Imm+R[rb]+R[ri]sImm+R[r_b]+R[r_i]*s

image-20221128082359267.png

【例】书中例子:假设寄存器和内存地址的值如下

image-20221201081346924.png

那么所示操作数的值如下:

image-20221201081441398.png

3.4.2 数据传送指令——MOV

  • mov:将数据从源位置复制到目的位置。根据操作数据大小,分为四种情况:movbmovwmovlmovq
movl $0x4050, %eax        //4 bytes
movw %bp, %sp             //2 bytes
movb (%rdi, %rcx), %al    //1 byte
movb $-17, (%rsp)         //1 byte
movq %rax, -12(%rbp)      //8 bytes

【例】mov立即数

movabsq $0x0011223344556677, %rax  //%rax = 0011223344556677
movb    $-1, %al                   //%rax = 00112233445566FF
movw    $-1, %al                   //%rax = 001122334455FFFF
movl    $-1, %al                   //%rax = 00000000FFFFFFFF
movq    $-1, %al                   //%rax = FFFFFFFFFFFFFFFF

【注】

  1. 使用movl时,会把寄存器的高4位字节设置为0
  2. 常规的movq只能以32位补码作为源立即数。64位立即数要用movabsq,且只能以寄存器作为目的位置
  • 移动指令:较小的源复制到较大的目的时使用
    • movz:零扩展,剩余位填充0(2.2.6节零扩展)。如:movzbw表示byte字节领扩展到word
    • movs:符号扩展,剩余位按符号扩展填充(2.2.6节符号扩展)
    • cltq:相当于movslq %eax, %rax
    • 注意:movzlq不存在,原因是与movl相同

image-20221201081735001.png

【例】书中例子

movabsq $0x0011223344556677, %rax  //%rax = 0011223344556677
movb    $0xAA, %dl                 //%dl = AA
movb    %dl, %al                   //%rax = 00112233445566AA
movsbq  %dl, %rax                  //%rax = FFFFFFFFFFFFFFAA
movzbq  %dl, %rax                  //%rax = 00000000000000AA

3.4.3 C语言的mov

long exchange(long *xp, long y)
{
    long x = *xp;
    *xp = y;
    return x;
}

【编译】gcc -Og -S exchange.c

// long exchange(long *xp, long y)
// xp in %rdi, y in %rsi
exchange:
    movq (%rdi), %rax
    movq %rsi, (%rdi)
    ret

【说明】C语言的指针其实对应的就是取内存地址

3.4.4 压入和弹出栈数据

  • 后进先出原则的数据结构。可以看做一个数组,分两个方向:栈顶和栈底
    • 栈底:高地址
    • 栈顶:低地址,属于插入和删除数据的一端。%rsp保存栈顶元素的地址
  • 栈向下增长,栈顶元素的地址一般是最低的

image-20221201211136186.png

【说明】

  • 过程:
    • 假设栈已经有一些数据,栈顶是0x108
    • 此时执行pushq,则栈顶-8字节,寄存器值填入对应栈的内存
    • 此时执行popq,则栈顶+8字节,读取内存栈的内存到寄存器
  • pushq %rax:将四字节数据压栈。等价于以下两条命令:
subq $8, %rsp        //Decrement stack pointer
movq %rax, (%rsp)    //Store %rax on stack
  • popq %rdx:栈弹出四字节数据。等价于以下两条命令:
movq (%rsp), %rdx    //Read %rdx from stack
addq $8, %rsp        //Increment stack pointer

3.5 算数和逻辑操作

image-20221201215203576.png

3.5.1 加载有效地址leaq

leaq指令形式:leaq (%rdi, %rsi, 4), %rax

movq区别:第一个指令看起来是内存形式,但是其实没有读取内存,而是直接将有效地址写到目的操作数

【理解】第一个指令是内存形式的话,相当于利用了Imm+R[rb]+R[ri]sImm+R[r_b]+R[r_i]*s 简化了指令运算

【例】书中例子,使用leaq简化函数的算数计算

long scale(long x, long y, long z) {
    long t = x + 4 * y + 12 * z;
    return t;
}

【编译】

//long scale(long x, long y, long z)
//x in %rdi, y in %rsi, z in %rdx
scale:
    leaq (%rdi,%rsi,4), %rax        //rax = x+4*y
    leaq (%rdx,%rdx,2), %rdx        //rdx = z+2*z = 3z
    leaq (%rax,%rdx,4), %rax        //rax = (x+4*y) + 4*(3z) = x+4*y+12*z
    ret

3.5.2 一元和二元操作

【例】书中例子

  • incq (%rsp):使得栈顶元素+1,即C语言的++运算
  • subq %rax, %rdx:rdx-rax的结果赋值给rdx,即C语言的x-=y

【注】一元和二元操作的目的操作数不能为立即数(理解为立即数不能作为左值,寄存器和内存地址可以)

3.5.3 移位操作

  • 左移:算数左移(SAL)、逻辑左移(SHL),效果一样,都是右边填0
  • 右移:算数右移(SAR,填0)、逻辑右移(SHR,填符号)

【注】移位量可以是立即数,或单字节存放在寄存器%cl

【例】书中例子,使用leaqsalq代替整数乘法。使用mulq太费时间

leaq (%rdx,%rdx,2), %rax    //rax = rdx+rdx*2 = 3*rdx
salq $4, %rax               //rax = rax<<4 = 48*rdx

3.5.5 特殊的算数操作

  • 乘法运算和除法运算:由于结果溢出,存放到两个寄存器raxrdx中:

image-20221202062011773.png

【例】书中例子

#include <inttypes.h>
typedef unsigned __int128 uint128_t;
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y) {
    *dest = x * (uint128_t)y;
}

【编译】

//void store_uprod(uint128_t *dest, uint64_t x, uint64_t y)
//dest in %rdi, x in %rsi, y in %rdx
store_uprod:
    movq %rsi, %rax
    mulq %rdx
    movq %rax, (%rdi)
    movq %rdx, 8(%rdi)
    ret