# 4.1.5 Y86-64 程序

图 4-6 给出了下面这个 C 函数的 x86-64 和 Y86-64 汇编代码:

long sum(long *start, long count)
{
    long sum = 0;
    while (count) {
        sum += *start;
        start++;
        count--;
    }
    return sum;
}

x86-64 代码

# long sum(long *start, long count)
# start in %rdi, count in %rsi
sum:
    movl    $0, %eax          # sum = 0
    jmp     .L2               # Goto test
.L3:                          # loop:
    addq    (%rdi), %rax      # Add *start to sum
    addq    $8, %rdi          # start++
    subq    $1, %rsi          # count--
.L2:                          # test:
    testq   %rsi, %rsi        # Test sum
    jne     .L3               # If !=0, goto loop
    rep; ret                  # Return

Y86-64 代码

# long sum(long *start, long count)
# start in %rdi, count in %rsi
sum:
    irmovq  $8, %r8           # Constant 8
    irmovq  $1, %r9           # Constant 1
    xorq    %rax, %rax        # sum = 0
    andq    %rsi, %rsi        # Set CC
    jmp     test              # Goto test
loop:
    mrmovq  (%rdi), %r10      # Get *start
    addq    %r10, %rax        # Add to sum
    addq    %r8, %rdi         # start++
    subq    %r9, %rsi         # count--. Set CC
test:
    jne     loop              # Stop when 0
    ret                       # Return

图 4-6 Y86-64 汇编程序与 x86-64 汇编程序比较。Sum 函数计算一个整数数组的和。Y86-64 代码与 x86-64 代码遵循了相同的通用模式

x86-64 代码是由 GCC 编译器产生的。Y86-64 代码与之类似,但有以下不同点:

  • Y86-64 将常数加载到寄存器(第 2~3 行),因为它在算术指令中不能使用立即数。
  • 要实现从内存读取一个数值并将其与一个寄存器相加,Y86-64 代码需要两条指令(第 8~9 行),而 x86-64 只需要一条 addq 指令(第 5 行)。
  • 我们手工编写的 Y86-64 实现有一个优势,即 subq 指令(第 11 行)同时还设置了条件码,因此 GCC 生成代码中的 testq 指令(第 9 行)就不是必需的。不过为此,Y86-64 代码必须用 andq 指令(第 5 行)在进入循环之前设置条件码。

图 4-7 给出了用 Y86-64 汇编代码编写的一个完整的程序文件的例子。这个程序既包括数据,也包括指令。伪指令(directive)指明应该将代码或数据放在什么位置,以及如何对齐。这个程序详细说明了栈的放置、数据初始化、程序初始化和程序结束等问题。

# Execution begins at address 0
    .pos 0
    irmovq stack, %rsp        # Set up stack pointer
    call main                 # Execute main program
    halt                      # Terminate program

# Array of 4 elements
    .align 8
array:
    .quad 0x000d000d000d
    .quad 0x00c000c000c0
    .quad 0x0b000b000b00
    .quad 0xa000a000a000

main:
    irmovq array, %rdi
    irmovq $4, %rsi
    call sum                  # sum(array, 4)
    ret

# long sum(long *start, long count)
# start in %rdi, count in %rsi
sum:
    irmovq $8, %r8            # Constant 8
    irmovq $1, %r9            # Constant 1
    xorq %rax, %rax           # sum = 0
    andq %rsi, %rsi           # Set CC
    jmp test                  # Goto test
loop:
    mrmovq (%rdi), %r10       # Get *start
    addq %r10, %rax           # Add to sum
    addq %r8, %rdi            # start++
    subq %r9, %rsi            # count--. Set CC
test:
    jne loop                  # Stop when 0
    ret                       # Return

# Stack starts here and grows to lower addresses
    .pos 0x200
stack:

图 4-7 用 Y86-64 汇编代码编写的一个例子程序。调用 sum 函数来计算一个具有 4 个元素的数组的和

在这个程序中,以“.”开头的词是汇编器伪指令(assembler directives),它们告诉汇编器调整地址,以便在那里产生代码或插入一些数据。伪指令 .pos 0(第 2 行)告诉汇编器应该从地址 0 处开始产生代码。这个地址是所有 Y86-64 程序的起点。接下来的一条指令(第 3 行)初始化栈指针。我们可以看到程序结尾处(第 40 行)声明了标号 stack,并且用一个 .pos 伪指令(第 39 行)指明地址 0x200。因此栈会从这个地址开始,向低地址增长。我们必须保证栈不会增长得太大以至于覆盖了代码或者其他程序数据。

程序的第 8~13 行声明了一个 4 个字的数组,值分别为

0x000d000d000d, 0x00c000c000c0
0x0b000b000b00, 0xa000a000a000

标号 array 表明了这个数组的起始,并且在 8 字节边界处对齐(用 .align 伪指令指定)。第 16~19 行给出了“main”过程,在过程中对那个四字数组调用了 sum 函数,然后停止。

正如例子所示,由于我们创建 Y86-64 代码的唯一工具是汇编器,程序员必须执行本来通常交给编译器、链接器和运行时系统来完成的任务。幸好我们只用 Y86-64 来写一些小的程序,对此一些简单的机制就足够了。

图 4-8 是 YAS 的汇编器对图 4-7 中代码进行汇编的结果。为了便于理解,汇编器的输出结果是 ASCII 码格式。汇编文件中有指令或数据的行上,目标代码包含一个地址,后面跟着 1~10 个字节的值。

我们实现了一个指令集模拟器,称为 YIS,它的目的是模拟 Y86-64 机器代码程序的执行,而不用试图去模拟任何具体处理器实现的行为。这种形式的模拟有助于在有实际硬件可用之前调试程序,也有助于检查模拟硬件或者在硬件上运行程序的结果。用 YIS 运行例子的目标代码,产生如下输出:

Stopped in 34 steps at PC = 0x13.  Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax:  0x0000000000000000    0x0000abcdabcdabcd
%rsp:  0x0000000000000000    0x0000000000000200
%rdi:  0x0000000000000000    0x0000000000000038
%r8:   0x0000000000000000    0x0000000000000008
%r9:   0x0000000000000000    0x0000000000000001
%r10:  0x0000000000000000    0x0000a000a000a000

Changes to memory:
0x01f0: 0x0000000000000000    0x0000000000000055
0x01f8: 0x0000000000000000    0x0000000000000013

模拟输出的第一行总结了执行以及 PC 和程序状态的结果值。模拟器只打印出在模拟过程中被改变了的寄存器或内存中的字。左边是原始值(这里都是 0),右边是最终的值。从输出中我们可以看到,寄存器 %rax 的值为 0xabcdabcdabcd,即传给子函数 sum 的四元素数组的和。另外,我们还能看到栈从地址 0x200 开始,向下增长,栈的使用导致内存地址 0x1f0~0x1f8 发生了变化。可执行代码的最大地址为 0x090,所以数值的入栈和出栈不会破坏可执行代码。

练习题 4.3 机器级程序中常见的模式之一是将一个常数值与一个寄存器相加。利用目前已给出的 Y86-64 指令,实现这个操作需要一条 irmovq 指令把常数加载到寄存器,然后一条 addq 指令把这个寄存器值与目标寄存器值相加。假设我们想增加一条新指令 iaddq,格式如下:

指令 字节 0 字节 1 字节 2~9
iaddq V, rB C:0 F:rB V

该指令将常数值 V 与寄存器 rB 相加。

使用 iaddq 指令重写图 4-6 的 Y86-64 sum 函数。在之前的代码中,我们用寄存器 %r8%r9 来保存常数值。现在,我们完全可以避免使用这些寄存器。

0x000:                      | # Execution begins at address 0
0x000:                      |     .pos 0
0x000: 30f40002000000000000 |     irmovq stack, %rsp       # Set up stack pointer
0x00a: 803800000000000000   |     call main                # Execute main program
0x013: 00                   |     halt                     # Terminate program
                            |
                            | # Array of 4 elements
0x018:                      |     .align 8
0x018:                      | array:
0x018: 0d000d000d000000     |     .quad 0x000d000d000d
0x020: c000c000c0000000     |     .quad 0x00c000c000c0
0x028: 000b000b000b0000     |     .quad 0x0b000b000b00
0x030: 00a000a000a00000     |     .quad 0xa000a000a000
                            |
0x038:                      | main:
0x038: 30f71800000000000000 |     irmovq array, %rdi
0x042: 30f60400000000000000 |     irmovq $4, %rsi
0x04c: 805600000000000000   |     call sum                 # sum(array, 4)
0x055: 90                   |     ret
                            |
                            | # long sum(long *start, long count)
                            | # start in %rdi, count in %rsi
0x056:                      | sum:
0x056: 30f80800000000000000 |     irmovq $8, %r8           # Constant 8
0x060: 30f90100000000000000 |     irmovq $1, %r9           # Constant 1
0x06a: 6300                 |     xorq %rax, %rax          # sum = 0
0x06c: 6266                 |     andq %rsi, %rsi          # Set CC
0x06e: 708700000000000000   |     jmp test                 # Goto test
0x077:                      | loop:
0x077: 50a70000000000000000 |     mrmovq (%rdi), %r10      # Get *start
0x081: 60a0                 |     addq %r10, %rax          # Add to sum
0x083: 6087                 |     addq %r8, %rdi           # start++
0x085: 6196                 |     subq %r9, %rsi           # count--. Set CC
0x087:                      | test:
0x087: 747700000000000000   |     jne loop                 # Stop when 0
0x090: 90                   |     ret                      # Return
                            |
                            | # Stack starts here and grows to lower addresses
0x200:                      |     .pos 0x200
0x200:                      | stack:

图 4-8 YAS 汇编器的输出。每一行包含一个十六进制的地址,以及字节数在 1~10 之间的目标代码

练习题 4.4 根据下面的 C 代码,用 Y86-64 代码来实现一个递归求和函数 rsum

long rsum(long *start, long count)
{
    if (count <= 0)
        return 0;
    return *start + rsum(start+1, count-1);
}

使用与 x86-64 代码相同的参数传递和寄存器保存方法。在一台 x86-64 机器上编译这段 C 代码,然后再把那些指令翻译成 Y86-64 的指令,这样做可能会很有帮助。

练习题 4.5 修改 sum 函数的 Y86-64 代码(图 4-6),实现函数 absSum,它计算一个数组的绝对值的和。在内循环中使用条件跳转指令。

练习题 4.6 修改 sum 函数的 Y86-64 代码(图 4-6),实现函数 absSum,它计算一个数组的绝对值的和。在内循环中使用条件传送指令。