03程序的机器级表示

机器级编程的抽象

指令集体系结构

定义了处理器状态、指令的格式、每条指令对状态的影响

大多数ISA将程序的行为描述成好像每条指令是按顺序执行的,一条指令结束后,下一条再开始

处理器的硬件远比描述的精细复杂,它们并发执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致

虚拟地址

虚拟地址使得存储器模型看上去是一个非常大的字节数组,将多个硬件存储器和操作系统软件组合起来

汇编语言

循环

大多数汇编器根据一个循环的do-while形式来产生循环代码,其他循环会首先转换成do-while形式,然后再编译成机器代码

do-while
do {
    body-statement;
} while (test-expr);

// 翻译成goto
loop:
    body-statement;
    t = text-expr;
    if (t)
        goto loop;
int fact_do (int n) {
    int result = 1;
    do {
        result *= n;
        n = n - 1;
    } while (n > 1);
    return result;
}

    movl    8(%ebp), %edx   # get n
    movl    $1, %eax        # set result = 1
.L2:                    # loop:
    imull   %edx, %ax       # compute result *= n
    subl    $1, %edx        # decrement n
    cmpl    $1, %edx        # compare n: 1
    jg      .L2             # if >, goto loop
                            # return result
while
while (test-expr) {
    body-statement;
}

// 翻译成goto
    t = test-expr
    if (!t)
        goto done;
loop:
    body-statement;
    t = test-expr;
    if (t)
        goto loop;
done:
int fact_while (int n) {
    int result = 1;
    while (n > 1) {
        result *= n;
        n = n - 1;
    }
    return result;
}

    movl    8(%ebp), %edx   # get n
    movl    $1, %eax        # set result = 1
    cmpl    $1, %edx        # compare n: 1
    jle     .L7             # if <=, goto done
.L10:                   # loop:
    imull   %edx, %eax      # compute result *= n
    subl    $1, %edx        # decrement n
    cmpl    $1, %edx        # compare n: 1
    jg      .L10            # if >, goto loop
.L7:                    # done
                            # return result
for
for (init-expr; test-expr; update-expr) {
    body-statement;
}

// 翻译成goto
    init-expr
    t = test-expr;
    if (!t)
        goto done;
loop:
    body-statement;
    update-expr;
    t = test-expr;
    if (t)
        goto loop;
done:
int fact_for (int n) {
    int i;
    int result = 1;
    for (i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

    movl    8(%ebp), %ecx   # get n
    movl    $2, %edx        # set i to 2
    movl    $1, %eax        # set result = 1
    cmpl    $1, %ecx        # compare n: 1
    jle     .L14            # if <=, goto done
.L17:                   # loop:
    imull   %edx, %eax      # compute result *= i
    addl    $1, %edx        # increment i
    cmpl    %edx, %ecx      # compare n: i
    jge     .L17            # if >=, goto loop
.L14:                   # done
                            # return result

条件操作

控制的条件转移

机制简单通用

流水线上效率低

数据的条件转移

先计算一个条件操作的两种结果,然后根据条件是否满足从而选取一个

一般来说会改进代码效率

但是当分支需要大量计算,当相对应的条件不满足,工作白费

给GCC以命令行选项-march=i686开启

int absdiff (int x, int y) {
    return x < y ? y-x : x-y;
}

int cmovdiff (int x, int y) {
    int tval = y - x;
    int rval = x - y;
    int test = x < y;
    if (test)
        rval = tval;
    return rval;
}

    movl    8(%ebp), %ecx   # get x
    movl    12(%ebp), %edx  # get y
    movl    %edx, %ebx      # copy y
    subl    %ecx, %ebx      # compute y-x
    movl    %ecx, %eax      # copy x
    subl    %edx, $eax      # compute x-y and set as return value
    cmpl    %edx, %ecx      # compare x: y
    cmovl   %ebx, %eax      # if <, replace return value with y-x

switch

可以根据一个整数索引值进行多重分支

处理具有多种可能结果的测试时,这种语句特别有用,不仅提高了C代码可读性,而且使用跳转表这种数据结构使得实现更加高效

跳转表是一个数组,表相i时代码段的地址,这个代码段实现相当于开关索引值等于i时程序应该采取的动作

程序代码用开关索引值来执行一个跳转表内的数组饮用,确定跳转指令的目标


过程

一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部份。另外还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间

使用栈保存局部变量的原因

  • 没有足够多的寄存器存放所有的局部变量
  • 有些局部变量是数组或结构,必须通过数组或结构饮用来访问
  • 对一个局部变量使用地址操作符&,必须能够为它生成一个地址

寄存器使用惯例

  • 调用者保存寄存器:%eax,%edx,%ecx
  • 被调用者保存寄存器:%ebx,%esi,%edi
  • 必须保存寄存器:%ebp,%esp

数据对齐

许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值 K K (2、4、8)的倍数。这种对齐限制简化了行程处理器和存储器系统之间接口的硬件设计。SSE单元和存储器之间传送数据的指令要求存储器地址必须是16的倍数,栈帧的长度都是16字节的倍数(返回地址,保存的%esp分别占4字节)

Linux对齐策略

  • 2字节数据类型(short)的地址必须是2的倍数
  • 较大的数据类型(int、void*、float、double)的地址必须是4的倍数

Windows对齐策略

任何K字节基本对象的地址都必须是 K K (2、4、8)的倍数

  • double和long double数据类型的地址必须是8的倍数

存储器越界

数组引用不进行边界检查,而且局部变量和状态信息都存放在栈里,当程序破坏状态,试图重新加载寄存器或使用ret指令就会出现严重错误

缓冲区溢出

gets 和 fgets

  • gets(char*)函数不进行边界检查,会导致存储溢出

  • fgets(char*, int, FILE*)函数进行边界检查,不会导致存储溢出

缓冲区溢出攻击

缓冲区溢出更加致命的使用就是执行本来不愿意执行的函数,这是最常见的通过计算机网络攻击系统安全的方法

一种攻击形式,攻击代码会使用系统调用启动一个外壳程序,给攻击者提供一组操作系统函数

另一种攻击形式是,攻击代码会执行一些未授权的人物,修复对栈的破坏,然后第二次执行ret指令,表面上正常返回给调用者

对抗缓冲区溢出攻击

  • 栈随机化

    使得栈的位置在程序每次运行时都有变化。Linux使用地址空间布局随机化(ASLR),每次运行时程序的不同部分,包括程序代码、库代码、栈、全局变量、堆数据都会加载到存储器不同区域

  • 栈破坏检测

    最近的GCC版本在产生的代码中加入了一种栈保护者机制,用来检测缓冲区越界。其思想是在栈帧种人和局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,在程序每次运行时随机产生。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个操作改变了,如果是,程序异常终止

    使用命令行选项-fno-stack-protector来阻止GCC产生这种代码

  • 限制可执行代码区域

    一种方法是限制那些能够存放可执行代码的存储器区域。在典型的程序中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其它部分可以被限制为只允许读和写

x86-64

特性

数据类型

  • 指针从32位变成64位
  • 长整型(long)从32位变成64位

访问信息

64位寄存器

  • 寄存器数量从8个翻倍到16个
  • 寄存器都是64位长,IA32寄存器的64位扩展分别命名为%rax,%rcx,%rdx,%rbx,%rsi,%rdi,%rsp,%rbp。新增的寄存器命名为%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15
  • 可以直接访问每个寄存器的低32位:%eax,%ecx,%edx,%ebx,%esi,%edi,%esp,%ebp,%r8d,%r9d,%r10d,%r11d,%r12d,%r13d,%r14d,%r15d
  • 可以直接访问每个寄存器的低16位:%ax,%cx,%dx,%bx,%si,%di,%sp,%bp,%r8w,%r9w,%r10w,%r11w,%r12w,%r13w,%r14w,%r15w
  • 可以直接访问每个寄存器的低8位:%al,%cl,%dl,%bl,%sil,%dil,%spl,%bpl,%r8b,%r9b,%r10b,%r11b,%r12b,%r13b,%r14b,%r15b
  • %rsp有特殊的状态,会保存指向栈顶元素的指针
  • 没有帧指针寄存器
  • 可以用%rbp作为通用寄存器
  • 基址寄存器和变址寄存器必须使用寄存器的r版本

算术指令

  • 增加了在四字上进行运算的指令,后缀为q

控制

  • 增加了在四字上进行比较合测试的指令,后缀为q

过程

  • 参数(最多前六个参数)可以通过寄存器传递到过程(%rdi,%rsi,%rdx,%rcx,%r8,%r9),而不是在栈上,消除了在栈上存储和检索值的开销
  • callq指令将一个64位返回地址存储在栈上
  • 许多函数不需要栈帧,只有那些不能将所有局部变量都存放在寄存器的函数才需要在栈上分配空间
  • 函数最多可以访问超过当前栈指针值128个字节的栈上存储空间
  • 没有帧指针,作为代替,对栈位置的引用相对于栈指针

栈帧

需要栈的唯一原因就是用来保存返回地址

可能需要栈帧的原因

  • 局部变量太多,不能都放在寄存器
  • 有些局部变量是数组或结构
  • 函数用取地址操作符来计算一个局部变量的地址
  • 函数必须将栈上的某些参数传递到另一个函数
  • 在修改一个被调用者保存寄存器之前,函数需要保存它的状态

数据结构

  • 数组作为同样大小的块的序列来分配(同IA32一样)
  • 结构作为变长的块来分配(同IA32一样)
  • 联合作为一个单独的块来分配,这个块足够大能够装下联合中最大的元素(同IA32一样)
  • 对于任何需要K字节的标量数据类型来说,它的起始地址必须是 K K <script type="math/tex" id="MathJax-Element-93">K</script>的整数倍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值