程序的机器级表示(一)汇编,汇编格式和数据传输指令

系列文章

: 深入理解计算机系统笔记

文章目录

  • 系列文章
  • [3 程序的机器级表示](#3 程序的机器级表示)
    • [3.1 历史观点](#3.1 历史观点)
    • [3.2 程序编码](#3.2 程序编码)
      • [3.2.1 机器级代码](#3.2.1 机器级代码)
      • [3.2.2 代码示例](#3.2.2 代码示例)
      • [3.2.3 关于格式的注解](#3.2.3 关于格式的注解)
    • [3.3 数据格式](#3.3 数据格式)
    • [3.4 访问信息](#3.4 访问信息)
      • [3.4.1 操作数指示符](#3.4.1 操作数指示符)
      • [3.4.2 数据传送指令](#3.4.2 数据传送指令)
      • [3.4.3 数据传送示例](#3.4.3 数据传送示例)
      • [3.4.4 压入和弹出栈数据](#3.4.4 压入和弹出栈数据)

3 程序的机器级表示

  • 计算机执行机器代码 ,编译器基于编程语言的规则、目标机器的指令集,操作系统遵循的惯例生成机器代码。
  • 汇编代码是机器代码的文本表示 。高级代码可移植性较好,而汇编代码与特定机器密切相关
  • 现在不要求使用汇编语言编制程序,能够阅读和理解编译器转化的汇编语言的细节和方式,并分析代码中隐含的低效率。
  • 精通细节是理解更深和更基本概念的先决条件

3.1 历史观点

  • Intel处理器系列俗称x86,每个后续处理器都是向下兼容 的(所以指令集中会有一些奇怪的东西),x86(64位)
  • 摩尔定律: 晶体管数目18个月翻一番。

3.2 程序编码

  • 使用较高级别优化 的代码会严重变形(和源代码的格式),机器代码和初始源代码之间的关系难以理解 。实际中,从程序性能 考虑,较高级别的优化是较好的选择(O2用的比较多)。
  • 汇编器产生的目标代码是机器代码的一种形式 ,它包含二进制形式表示的所有指令,但还没有填入全局值的地址。链接之后才形成可执行代码,可执行代码是机器代码的第二种形式

3.2.1 机器级代码

  • 对机器级编程尤为重要的两种抽象
    1.指令集架构:定义了处理器状态、指令的格式、指令对状态的影响。
  1. 虚拟地址:机器级程序使用虚拟地址,即将内存看成一个按字节寻址的数组。
  • 一些通常对语言级隐藏的处理器状态(机器级可见)
  1. 程序计数器(PC) :下一条执行指令的地址
  2. 整数寄存器文件:保存临时数据或重要的程序状态
  3. 条件码寄存器:最近执行的算术或逻辑指令的状态信息
  4. 一组向量寄存器:保存一个或多个整数或浮点数值
  • 机器代码和汇编代码中不区分有符号数和无符号数,不区分指针的不同类型,不区分指针和整数。
  • 因为虚拟内存的大小通常比较大,程序实际使用和访问的内存大小通常远小于虚拟内存看起来的大小。所以在任意的时刻,只有有限的虚拟内存是合法的,操作系统负责管理虚拟内存(通过表翻译为实际的物理地址)。
  • 一条机器指令只执行一个非常基本的操作。

3.2.2 代码示例

c 复制代码
#include <stdio.h>

// 声明 multstore 函数
void multstore(long x, long y, long *dest);

// 声明 mult2 函数
long mult2(long a, long b);

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

// 定义 multstore 函数
void multstore(long x, long y, long *dest) {
    *dest = mult2(x, y);
}

// 定义 mult2 函数
long mult2(long a, long b) {
    long s = a * b;
    return s;
}

gcc -S a.c -o multstore.s

asm 复制代码
 //部分汇编,不同优化等级和环境产生的不一样
 //这个和书上差别有亿点大
 63 mult2:
 64 .LFB2:
 65     .cfi_startproc
 66     pushq   %rbp
 67     .cfi_def_cfa_offset 16
 68     .cfi_offset 6, -16
 69     movq    %rsp, %rbp
 70     .cfi_def_cfa_register 6
 71     movq    %rdi, -24(%rbp)
 72     movq    %rsi, -32(%rbp)
 73     movq    -24(%rbp), %rax
 74     imulq   -32(%rbp), %rax
 75     movq    %rax, -8(%rbp)
 76     movq    -8(%rbp), %rax
 77     popq    %rbp
 78     .cfi_def_cfa 7, 8
 79     ret
 80     .cfi_endproc
  • -S选项产生汇编代码
  • 反汇编是根据机器代码反推出汇编的,逆向和一些安全漏洞分许就会用到这个
  • 机器代码与反汇编表示的特性:
  1. x86-64 的指令长度范围为 1~15 字节常用指令和操作数少的指令所需字节少。
  2. 指令格式设计方式 为:可以将字节唯一的解码成机器指令。
  3. 反汇编器基于机器代码文件中的字节序列确定汇编代码,与源代码和编译时的汇编代码无关
  4. 指令结尾的 'q' 是大小指示符,大多数情况下可以省略。
  • 从源程序转换来的可执行目标文件中,除了程序过程的代码,还包含启动和终止程序的代码,与操作系统交互的代码。

3.2.3 关于格式的注解

asm 复制代码
 81 .LFE2:
 82     .size   mult2, .-mult2
 83     .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
 84     .section    .note.GNU-stack,"",@progbits

像这样的汇编代码,以 '.' (点) 开头的行是指导汇编器和链接器工作的伪指令。我们一般忽略它们。

  • 在汇编语言中,Intel 和 AT&T 是两种主要的语法格式它们在指令格式、操作数顺序、寄存器命名等方面有显著的区别。
  1. Intel 语法: 目的操作数在前,源操作数在后。
    AT&T 语法: 源操作数在前,目的操作数在后。
  2. 操作数大小
    Intel 语法 : 操作数大小由操作码决定 ,不需要额外的后缀。
    AT&T 语法: 使用后缀来指明操作数大小(b 表示字节,w 表示字,l 表示双字,q 表示四字)。
  3. 寄存器命名
    Intel 语法: 寄存器名称直接使用。
    AT&T 语法: 寄存器名称前面加 % 符号。
  4. 立即数
    Intel 语法: 立即数不需要前缀。
    AT&T 语法: 立即数前面加 $ 符号
  • 还有一些符号上的小差距,总的来说两者操作数顺序恰好相反,
  • 我个人觉得Intel语法在许多方面更加简洁
  • 有些C语言访问不到的机器特性,我们可以考虑包含(asm伪指令)或者链接一部分汇编指令来优化程序

3.3 数据格式

汇编代码指令最后一个字符的后缀 :movb, movw, movl, movq。

这里说的都是整数,浮点数使用一组完全不同的指令和寄存器,"l"既可以表示四字节整数,也可以表示8字节的双精度浮点数。

3.4 访问信息

  • 名称
  1. 起初的8086 只有8个16位的寄存器:%ax到%bp (r是特殊的栈指针)
  2. 后面IA32架构扩展至32位,前缀一个e,也就是%eax到%ebx
  3. x86-64架构扩展至16个64位 ,自带一个r,大小由尾缀决定 ,编号也挺草率的,几个版本主打一个风格迥异
  • 低位操作的规则
  1. 将寄存器作为目标位置时,生成字节和字的指令会保持剩下的字节不变。(放字节,字(2字节)的时候就不改其他位的值了)
  2. 生成双字的指令会把高位四字节置为 0 。(32位扩展 的一部分内容
  • 16个寄存器的作用
    a:返回值
    s:栈指针
    d, s, d, c, 8, 9:第 1 到第 6 个参数
    b,bp, 12~15:被调用者保存
    10, 11:调用者保存

3.4.1 操作数指示符

  • 三种主要的操作数类型:
  1. 立即数 (Immediate) ,表示常数值
    考研好像是Intel格式
asm 复制代码
//Intel 语法示例
mov eax, 10   ; 将立即数 10 移动到寄存器 eax
add eax, 5    ; 将立即数 5 加到寄存器 eax
//AT&T 语法示例
movl $10, %eax   ; 将立即数 10 移动到寄存器 eax
addl $5, %eax    ; 将立即数 5 加到寄存器 eax
  1. 寄存器 (Register) ,使用寄存器中的全部位或者低位的内容
asm 复制代码
//Intel 语法示例
mov eax, ebx   ; 将寄存器 ebx 的值移动到寄存器 eax
add eax, ecx   ; 将寄存器 ecx 的值加到寄存器 eax
//AT&T 语法示例
movl %ebx, %eax   ; 将寄存器 ebx 的值移动到寄存器 eax
addl %ecx, %eax   ; 将寄存器 ecx 的值加到寄存器 eax
  1. 内存引用 (Memory Reference) ,寻址 ,可以是直接地址、间接地址或基于寄存器的地址计算。带了()或者[],和解引用指针很像
asm 复制代码
//Intel 语法示例
mov eax, [ebx]          ; 将内存地址 [ebx] 的值移动到寄存器 eax
mov [ecx + 4], edx      ; 将寄存器 edx 的值移动到内存地址 [ecx + 4]
add eax, [esi + edi*4]  ; 将内存地址 [esi + edi*4] 的值加到寄存器 eax
//AT&T 语法示例
movl (%ebx), %eax            ; 将内存地址 (%ebx) 的值移动到寄存器 eax
movl %edx, 4(%ecx)           ; 将寄存器 edx 的值移动到内存地址 4(%ecx)
addl (%esi, %edi, 4), %eax   ; 将内存地址 (%esi, %edi, 4) 的值加到寄存器 eax
  • 最后一种最常用也最重要(其他格式是它的一个特例)
  • Imm(rb, ri, s)
  • Imm(立即数偏移) + R[rb] (基址) + R[ri] (变址)s (比例因子)
  • s 只能是 1,2,4,8 中的一个

3.4.2 数据传送指令

  • 简单的四种mov指令
    movb, movw, movl,movq:传送字节、字、双字、四字
  • movabsq (move absolute quadword):传送绝对的四字 。用于将一个 64 位的立即数 传送到一个 64 位寄存器中。用于初始化寄存器或处理大数机器码九字节(1+8),较大。
  • mov的五种组合:
  1. 立即数到寄存器 (Immediate to Register)
    将一个立即数传送到一个寄存器中。
  2. 立即数到内存 (Immediate to Memory)
    将一个立即数传送到一个内存位置中。
  3. 寄存器到寄存器 (Register to Register)
    将一个寄存器的值传送到另一个寄存器中。
  4. 内存到寄存器 (Memory to Register)
    将一个内存位置的值传送到一个寄存器中。
  5. 寄存器到内存 (Register to Memory)
    将一个寄存器的值传送到一个内存位置中。
  • 示例
c 复制代码
	; Intel 语法
    ; 立即数到寄存器
    mov eax, 10
    ; 立即数到内存
    mov [var1], 20
    ; 寄存器到寄存器
    mov ebx, eax
    ; 内存到寄存器
    mov ecx, [var1]
    ; 寄存器到内存
    mov [var2], ebx
	; AT&T 语法
	; 立即数到寄存器
    movl $10, %eax
    ; 立即数到内存
    movl $20, var1
    ; 寄存器到寄存器
    movl %eax, %ebx
    ; 内存到寄存器
    movl var1, %ecx
    ; 寄存器到内存
    movl %ebx, var2
  • 较小的源值复制到较大的目的地 使用movz或者movs
    他们的后缀字符第一个指定源的大小,第二个指定目的大小
    movz,将剩余部分填充为0。
    movs,将剩余部分填充为符号位。

3.4.3 数据传送示例

  • 3.4.2已经示范差不多了
  • 局部变量通常保存在寄存器中。
  • 函数返回指令 ret 返回的值为寄存器 rax 中的值
  • 强制类型转换可通过 mov 指令实现的。
  • 当指针存在寄存器中时,a = p 的汇编指令为: mov (rdi), rax

3.4.4 压入和弹出栈数据

  • 栈:向下增长(所以压栈时减[%rsp]),后进先出
  • push:压栈
  • pop:出栈
  • %rsp :(64位) 栈指针,栈顶元素的地址
  • 指令尾缀代表操作的大小(bwlq)
  • 其实压栈操作等价于先减栈指针值,再将指定寄存器的值写入栈 ,反之,出栈先读出栈顶数据到指定寄存器,在加栈指针的值。而push,pop只被编码为一个字节即可完成这两步需要8个字节指令大小的操作。
  • 使用 mov 指令和标准的内存寻址方法可以访问栈内的任意位置,而非仅限于栈顶。
相关推荐
冷眼看人间恩怨2 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
Hejjon8 小时前
SpringBoot 整合 SQLite 数据库
笔记
西洼工作室10 小时前
【java 正则表达式 笔记】
java·笔记·正则表达式
初学者7.10 小时前
Webpack学习笔记(2)
笔记·学习·webpack
新手上路狂踩坑12 小时前
Android Studio的笔记--BusyBox相关
android·linux·笔记·android studio·busybox
stm 学习ing13 小时前
HDLBits训练3
c语言·经验分享·笔记·算法·fpga·eda·verilog hdl
尘觉13 小时前
算法的学习笔记—扑克牌顺子(牛客JZ61)
数据结构·笔记·学习·算法
bohu8314 小时前
sentinel学习笔记1-为什么需要服务降级
笔记·学习·sentinel·滑动窗口
初学者7.15 小时前
Webpack学习笔记(3)
笔记·学习·webpack
bohu8316 小时前
sentinel学习笔记5-资源指标数据统计
笔记·sentinel·statisticslot