csapp ch03 (part 1): Program Encoding and Basic Instructions

这篇文章是csapp第三章前五节的阅读笔记。

0x00 程序编码

对于两个C程序,可以通过如下的方式编译:

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

其中-Og选项告诉编译器不进行优化,直接根据C代码生成对应的汇编代码。

给出下面的C代码:

long mult2(long, long);

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

为了查看生成的汇编代码,在编译的时候可以加上-S选项:

gcc -Og -S mstore.c

结果如下:

        .file   "mstore.c"
        .text
        .globl  multstore
        .type   multstore, @function
multstore:
.LFB0:
        .cfi_startproc
        pushq   %rbx
        .cfi_def_cfa_offset 16
        .cfi_offset 3, -16
        movq    %rdx, %rbx
        call    mult2
        movq    %rax, (%rbx)
        popq    %rbx
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE0:
        .size   multstore, .-multstore
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-5)"
        .section        .note.GNU-stack,"",@progbits

其中以.开头的都是为了方便汇编器和链接器的内容。

如果使用-c选项的话,就可以生成.o.s文件了:

gcc -Og -c mstore.c

如果只有.o文件,那么也可以通过工具来生产对应的.s文件:

objdump -d mstore.o

结果:

mstore.o:     file format elf64-x86-64


Disassembly of section .text:

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

生成一个可执行文件需要对.o文件进行链接,其中需要有一个main函数。比如下面的main.c:

#include <stdio.h>

void multstore(long, long, long *);

int main() {
    long d;
    multstore(2, 3, &d);
    printf("2 * 3 --> %ld\n", d);
    return 0;
}

long mult2(long a, long b) {
    long s = a * b;
    return s;
}

编译可执行文件:

gcc -Og -o prog main.c mstore.c

就会生成一个名为prog的可执行文件。

可以对这个文件进行反编译:

objdump -d prog

其中就有我们之前看到的mstore.s的汇编代码:

0000000000400570 :
  400570:       53                      push   %rbx
  400571:       48 89 d3                mov    %rdx,%rbx
  400574:       e8 ed ff ff ff          callq  400566 
  400579:       48 89 03                mov    %rax,(%rbx)
  40057c:       5b                      pop    %rbx
  40057d:       c3                      retq   
  40057e:       66 90                   xchg   %ax,%ax

不同在于,在retq执行令之后还有一个指令,这是为了补齐,使得程序刚好是16字节的整数倍。

0x01 数据格式

不同的数据类型有不同的字节数,在汇编语言中,使用一个字母的后缀来表示对应指令操作的数据类型:

C declaration Intel data type Assembly-code suffix Size (bytes)
char Byte b 1
short Word w 2
int Double word l 4
long Quad word q 8
char* Quad word q 8
float Single precision s 4
double Double precison l 8

其中l对应两个类型:shortdouble

0x02 寄存器

x86-64的处理器有16个通用寄存器,每个寄存器都可以用来存储数字和指针:

这16个寄存器每一个还都可以分成小的来使用,比如%rax是8字节,%eax是4字节,%ax是2字节,%al是一个字节。

有一些寄存器有特殊的用途:

  • %rsp:栈指针;
  • %rax:返回值;
  • %rdi:第一个参数;
  • %rsi:第二个参数;
  • %rdx:第三个参数;
  • %rcx:第四个参数;
  • %r8:第五个参数;
  • %r9:第六个参数。

0x03 操作数指示符

寄存器是指令存放数据的地方,为了能够顺利执行指令,还需要指定对应的操作数。

操作数有三种来源:

  • 常量;
  • 来自寄存器;
  • 来自内存。

所以获取操作数的方式也有三大类:

Type Form Operand Value Name
Immediate $Imm Imm Immediate
Register ra R[ra] Register
Memory Imm M[Imm] Absolute
Memory (ra) M[R[ra]] Indirect
Memory Imm(rb) M[Imm+R[rb]] Base+displacement
Memory (rb,ri) M[R[rb]+R[ri]] Indexed
Memory Imm(rb,ri) M[Imm+R[rb]+R[ri]] Indexed
Memory (,ri,s) M[R[ri]·s] Scaled indexed
Memory Imm(,rj,s) M[Imm+R[rj]·s] Scaled indexed
Memory (rb,ri,s) M[R[rb]+R[ri]·s] Scaled indexed
Memory Imm(rb,ri,s) M[Imm+R[rb]+R[ri]·s] Scaled indexed

通过一个例子就能理解了。

这里是一些寄存器和内存中的数据:

Address Value Register Value
0x100 0xFF %rax 0x100
0x104 0xAB %rcx 0x1
0x108 0x13 %rdx 0x3
0x10c 0x11

下面给出获取操作数的过程:

Operand Type Value Note
%rax Register 0x100
0x104 Memory 0xAB M[Imm]
$0x108 Immediate 0x108
(%rax) Memory 0xFF M[R[ra]]
4(%rax) Memory 0xAB M[Imm+R[rb]]
9(%rax,%rdx) Memory 0x11 M[Imm+R[rb]+R[ri]]
260(%rcx,%rdx) Memory 0x13 M[Imm+R[rb]+R[ri]]
0xFC(,%rcx,4) Memory 0xFF M[Imm+R[rj]·s]
(%rax,%rdx,4) Memory 0x11 M[R[rb]+R[ri]·s]

0x04 数据传送指令

数据传输指令是使用非常频繁的指令,这些指令将数据从一个地方传送到另一个地方。

其中数据源可以是常量、寄存器和内存,目的地是寄存器和内存,不能是常量。

同时,还有一个规则,数据源和目的地不能都是内存

Instruction Effect Description
MOV S, D D <- S Move
movb Move byte
movw Move word
movl Move double word
movq Move quad word
movabsq I,R R <- I Move absolute quad word

最后一个指令movabsq是为了处理64位常数的,指令movq的数据源如果是常数的话只能是32位补码数,将这个数放入寄存器后,高位会使用补码的最高位填充。

而指令movabsq可以将任意64位的数放入寄存器中。

如果目的地是寄存器的话,大多数指令只是根据后缀标识的大小来覆盖相应寄存器中的数据,比如movb $FF %al只是将%rax中最后的一个字节置成0xFF

唯一的例外是movl,这个指令会将高位置成0:

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

当数据源的位数小于目的地时,对于高位有两种处理方式,补零,或者使用源数据的最高位填充。

补零:

Instruction Effect Description
MOVZ S, R R <- ZeroExtend(S) Move with zero extension
movzbw Move zero-extended byte to word
movzbl Move zero-extended byte to double word
movzbq Move zero-extended byte to quad word
movzwl Move zero-extended word to double word
movzwq Move zero-extended word to quad word

其中没有从double word到quad word的movzlq指令,因为movl指令默认就是补零。

使用最高位填充:

Instruction Effect Description
MOVS S, R R <- SignExtend(S) Move with sign extension
movsbw Move sign-extended byte to word
movsbl Move sign-extended byte to double word
movsbq Move sign-extended byte to quad word
movswl Move sign-extended word to double word
movswq Move sign-extended word to quad word
movslq Move sign-extended double to quad word
cltq %rax <- SignExtend(%eax) Sign-extended %eax to %rax

其中最后一个指令cltq没有操作数,这个指令和movslq %eax, %rax效果一样,不过更加精简。

例子:

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

0x05 数据传送示例

比如下面的C代码:

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

编译后的汇编代码如下:

exchange:
    .cfi_startproc
    movq    (%rdi), %rax
    movq    %rsi, (%rdi)
    ret

首先,函数的两个参数xpy分别存放在寄存器%rdi和%rsi中。

编译后一共两个指令,第一个指令是将寄存器%rdi地址中的数放入寄存器%rax中。

数据源是从内存中取数据,对应指针xp的解引用;

而寄存器%rax用于返回值,刚好对应return x

然后第二个指令将寄存器%rsi中的数放入内存中,地址在%rdi中。

对应语句*xp = y

从这里就可以看出,C语言中的指针仅仅是内存的地址;

指针的解引用就是将这个地址复制到寄存器中,然后使用那个寄存器来获取内存中的数据;

同样,函数的局部变量通常放在寄存器中,而不是放在内存里。

0x06 入栈和出栈

16个寄存器中有一个专门指栈地址的寄存器%rsp。

下面这两个指令就是和栈有关:入栈和出栈。

Instruction Effect Description
pushq S R[%rsp] <- R[%rsp]-8;M[R[%rsp]] <- S Push quad word
popq D D <- M[R[%rsp]];R[%rsp] <- R[%rsp]+8 Pop quad word

比如下面的例子:

0x07 算术和逻辑操作

下面列出了一些x86-64的整数和逻辑运算符:

Instruction Effect Description
leaq S, D D <- &S Load effective address
INC D D <- D+1 Increment
DEC D D <- D-1 Decrement
NEG D D <- -D Negate
NOT D D <- ~D Complement
Add S, D D <- D+S Add
SUB S, D D <- D-S Subtract
IMUL S, D D <- D*S Multiply
XOR S, D D <- D^S Exclusive-or
OR S, D `D <- D S`
AND S, D D <- D&S And
SAL k, D D <- D<<k Left shift
SHL k, D D <- D<<k Left shift (same as SAL)
SAR k, D D <- D>>_A k Arithmetic right shift
SHR k, D D <- D>>_L k Logical right shift

先来看看leaq指令。

这个指令其实是movq的变形,但是它将有效的地址复制到对应的寄存器中。

这个指令还可以用来进行算术运算。比如如果%rax中存放值x,那么leaq 7(%rdx,%rdx,4), %rax将计算5x+7

这个指令的目标操作数必须是一个寄存器。

下面是更多的例子(其中%rax中是x,%rcx中是y):

Instruction Result
leaq 6(%rax), %rdx 6+x
leaq (%rax,%rcx), %rdx x+y
leaq (%rax,%rcx,4), %rdx x+4y
leaq 7(%rax,%rax,8), %rdx 7+9x
leaq 0xA(,%rcx,4), %rdx 10+4y
leaq 9(%rax,%rcx,2), %rdx 9+x+2y

在上面的指令中,有的指令需要一个操作数,有的需要两个。

对于INC D指令来说,可以实现C语言中的++运算符。

对于ADD S, D指令来说,可以使用C语言中+=运算符。

0x08 移位操作

上面指令中的最后四个是关于移位操作的。

第一个操作数用来指定移动的位数,既可以是一个常数,也可以从寄存器%cl中读取。

寄存器%cl有一个字节,那么可以表示最大到255,而第二个操作数可以有不同的字节。

那么怎么来用%cl中的数来表示移动的位数而不会超过规定的最大位移数呢?

比如quad word是64位,那么就读取%cl中的最低6位就可以了;

对于double word的32位,读取%cl中的最低5位就可以了;

对于word的16位,读取%cl中的最低4位就可以了;

对于byte的8位,读取%cl中的最低3位就可以了。

0x09 特殊的算术操作

下面列出了一些特殊的算术操作:

Instruction Effect Description
imulq S R[%rdx]:R[%rax] <- S * R[%rax] Signed full multiply
mulq S R[%rdx]:R[%rax] <- S * R[%rax] Unsigned full multiply
cqto R[%rdx] <- SignExtend(R[%rax]) Convert to oct word
idivq S R[%rdx] <- R[%rdx]:R[%rax] mod S;R[%rax] <- R[%rdx]:R[%rax] / S Signed divide
divq S R[%rdx] <- R[%rdx]:R[%rax] mod S;R[%rax] <- R[%rdx]:R[%rax] / S Unsigned divide

其中oct word是16个字节。

需要注意的是,对于有符号和无符号的乘法和除法来说,指令下的操作方式是一样的。

这是因为不管是有符号还是无符号数,底层的运算规则都是一样的。

那么处理器就无法仅仅通过操作来判断是有符号还是无符号了,只能通过不同的指令名字来指明。

这就是编译过程中把C语言中的类型翻译成不同的指令来完成的。

后面还有一些这样的例子。


 Previous
csapp ch03 (part 2): Control csapp ch03 (part 2): Control
这篇文章是csapp第三章第六节的阅读笔记。 0x00 条件码控制语句需要进行条件判断,根据不同的判断值进行不同的操作。 这里涉及到的主要指令就是JUMP了。 不过在跳转之前,先看看如何来判断条件。 这就是条件码(Condition C
2020-06-10
Next 
ddia, Distributed Data (Part 2): Partitioning ddia, Distributed Data (Part 2): Partitioning
这篇文章是ddia第六章的阅读笔记。 0x00 Pre 第五章的复制有一个假设,数据副本可以在一台机器上存储。 如果不行的话,就需要将一个副本放在多个机器上了。 这就是分区(partitions),也叫分片(sharding)。 分区
2020-06-06
  You Will See...