程序的机器级表示

有些话、适合烂在心里 提交于 2019-12-01 07:28:24

机器级代码

在计算机中最终执行的都是机器代码,汇编代码、C 语言代码和高级语言的代码都需要转换成机器代码来执行。文章涉及的机器语言主要指 Intel IA32。

如下一段 C 语言代码:

1 int accum = 0;
2 
3 int sum(int x, int y) 
4 {
5    int t = x +y;
6    accum += t;
7    return ;    
8 }

 

通过 gcc -m32 -O1 -o code.o -c code.c 生成二进制格式的目标代码文件 code.o,通过 hexdump 查看文件内容,在计算机中最终执行的字节指令是:

通过 objdump -d code.o 反汇编查看这段二进制对应的汇编内容:

 

 

这里的指令 55 对应了汇编代码 push %ebp,汇编代码非常接近于机器代码,它用可读性更好的文本表示处理器执行的指令。

除了像上面那样通过反汇编目标文件查看对应的汇编代码,还可以通过 gcc 查看 C 编译器产生的汇编代码,如下命令会产生一个汇编文件 code.s 。

# -m32 表示用 32 位模式编译
gcc -O1 -m32 -S code.c

数据格式

虽然 C 语言可以在存储器中声明和分配各种数据类型的对象,但是机器代码只是简单地将存储器看成是一个很大的、按字节寻址的数组。C 语言中的聚合数据类型,例如数组和结构,在机器代码中只用连续的一组字节来表示。

下面是 C 语言基本数据类型对应的 IA32 表示

C声明 Intel数据类型 汇编代码后缀 字节大小
char 字节 b 1
short w 2
int 双字 l 4
long int 双字 l 4
char * 双字 l 4
float 单精度 s 4
double 双精度 l 8
long double 扩展精度 t 10/12

 

 

 

 

 

 

 

 

访问信息

寄存器

一个 IA32 CPU 包含一组 8 个存储 32 位值的寄存器,这些寄存器用来存储整数数据和指针。它们的名字都以 %e 开头,在最初的 8086 中,寄存器都是 16 位的。

%eax 是 32 位的,%ax 是 16 位的。所有 8 个寄存器可以作为 16 位或 32 位来访问,可以访问前 4 个寄存器的两个低位字节。

 

大多数系统的指令有一个或多个操作数,源数值可以是 常数、从寄存器或存储器中读出,结果可以存放在寄存器或存储器中。操作数可以分为 3 中类型:

1. 立即数,也就是常数值。用 “$”加一个整数表示,如 $550;

2. 寄存器,它表示某个寄存器的内容。用 E来表示任意寄存器 a,用 R[Ea] 表示它的值;

3. 存储器引用,它会根据计算出来的地址,访问某个存储器位置。用 Mb[Addr] 表示对存储在存储器中从地址 Addr 开始的 b 个字节的引用。

通用形式:Imm(Ea, Eb, s) 表示的操作数的有效地址为 M[Imm + R[Ea] + R[Eb]*s ] 。

数据传送指令

将数据从一个位置复制到另一个位置的指令是最频繁使用的指令。 

指令 描述
MOV   S,D 传送
MOVS  S,D 传送符号扩展的字节
MOVZ  S,D 传送零扩展的字节

这里两个操作数不能都指向存储器位置

算术和逻辑操作

指令 效果 描述
leal  S,D D <- &D

将有效地址读入寄存器

INC  D  D <- D+1 加 1
DEC  D D <- D - 1 减 1
NEG  D D <- -D 取负
NOT  D D <- ~D 取补
ADD  S,D D <- D+S
SUB  S,D D <- D-S
IMUL S,D D <- D*S
XOR  S,D D <- D^S 异或
OR    S,D D <- D | S
AND  S,D D <- D & S
SAL   k, D D <- D <<k 左移
SHL   k, D D <- D <<k 左移(同上)
SAR   k, D D <- D >>k 算术右移
SHR   k, D D <- D >>k 逻辑右移

与其他语言一样,程序的任何逻辑都是由基本指令在顺序执行条件执行循环执行组合得到。

除了整数寄存器,CPU 还维护着一组单个位的寄存器,它们描述了最近的算术或逻辑操作的属性(上面指令只有 leal 不改变任何条件码),可以检测这些寄存器来执行条件分支指令。

条件码 描述
CF

进位标志。检测 无符号溢出。

最近的操作使最高位产生了进位。

ZF

零标志。

最近操作得出的结果位0。

SF

符号标志。检测 负数。

最近操作得到的结果为负数。

OF

溢出标志。检测 有符号溢出。

最近的操作导致一个补码溢出 - 正溢出或负溢出。

 

除了算术和逻辑操作指令,还有下面两个指令只设置条件码而不改变任何其他寄存器。

指令 效果 描述
CMP  S2, S1 S- S2 比较两个操作数大小
TEST  S2, S1 S& S2 测试

条件码通常不会直接读取,常用使用方法有 3 种:

1. 根据条件码的某个组合,将一个字节设置为 0 或者 1;这类指令称为 SET 指令,它们之间的区别在于考虑的条件码组合情况。

2. 根据条件码跳转到程序的某个其他部分;

3. 可以有条件地传送数据。

跳转指令

指令 跳转条件 描述
jmp Label 1 无条件直接跳转
jmp *Operand 1 无条件间接跳转
je   Label ZF 相等/零 时跳转
jge Label ~(SF^OF) 大于或等于 时
...    

jmp *%eax 用寄存器 %eax 中的值作为跳转目标; jmp *(%eax) 以 %eax 中的值作为读地址,从存储器中读出跳转目标。

C 语言提供了多种循环结构,即 do-while、while 和 for,汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。

如通用 do-while 形式

1 do 
2    body-statement
3    while(test-expr)

可以通过下面组合完成

loop:
    body-statement
    t = test-expr;
    if (t)
        goto loop;

switch 语句

switch 通过一个整数索引值进行多重分支,处理具有多种可能结果的测试时特别有用,不仅提高了代码的可读性,通过跳转表使得实现更加高效。使用跳转表的优点是执行开关语句的时间与开关数量无关。
跳转表是一个数组,表项 i 是一个代码段的地址,当开关索引等于 i 时进行此部分代码段的操作。

 

过程(函数)

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

大多数机器,包括 IA32 只提供转移控制到过程和从过程中转移出控制这种简单的指令。数据传递、局部变量的分配和释放通过操纵程序栈来实现。

为单个过程分配的那部分栈称为栈帧(stack frame),栈帧的最顶端以两个指针界定,寄存器 %ebp 为帧指针(在一个过程中不变),寄存器 %esp 为栈指针(在一个过程执行中会移动)。

 

过程调用参数的常见步骤:

1. 保存原来的帧指针地址。在调用结束后,才能恢复调用者的帧指针,常见下面的代码

# 保存调用者的帧指针pushl  %ebp# 将 %ebp 设置为栈帧开始的位置
movl   %esp, %ebp

2. 为当前栈帧分配内存,常见汇编代码如下

subl $16, %esp

3. 备份调用者寄存器内容到栈,在返回时才能恢复调用者需要继续使用的寄存器内容。常见汇编代码如下

pushl %ebx

转移控制指令

指令 描述
call Label 过程直接调用。返回地址入栈,并跳转到被调用过程的起始处。
call *Operand 过程间接调用
leave 为返回准备栈
ret 从过程调用中返回。从栈中弹出地址,并跳转到这个位置。

 

寄存器的使用惯例

寄存器:%eax、%edx 和 %ecx 是调用者保存寄存器,当过程 P 调用 Q 时,Q 可以覆盖这些寄存器,而不会破坏任何 P 所需要的数据;

寄存器:%ebx、%esi 和 %edi 是被调用者保存寄存器,意味着 Q 必须在覆盖这些寄存器的值之前,先把它们保存到栈中,并在返回前恢复它们。

 

数组的分配和访问

对于数组 T A[N], L 为数据类型 T 的大小,首先它在存储器中分配一个 L*N 字节的连续区域,用 Xa 指向数组开头的指针,数组元素 i 会被存放在地址为 Xa+L*i 的地方。

嵌套数组是一个 T 为数组的数组,所以它满足数组分配和引用的一般原则。

 

结构

C 语言中用 struct 声明创建一个数据类型,将可能不同类型的对象聚合在一个对象中,结构的各个组成部分用名字来引用。

类似于数组,结构的所有组成部分都存放在存储器的一段连续区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段的偏移。机器代码不包含关于字段声明或字段名字的信息。

联合

允许以多种类型来引用一个对象。联合声明的语法与结构的语法一样,只不过语义相差比较大。它们是用不同的字段来引用相同的存储器块。

数据对齐

指针

以一种统一的方式,对不同数据结构中的元素产生引用。

1. 每个指针都对应一个类型,表明指针指向那一类对象。不过指针类型不是机器代码中的一部分;它指示 C 语言提供的一种抽象,帮助程序员避免寻址错误。

2. 每个指针都有一个值。是某个指定类型对象的地址。

3. 指针用 & 运算符创建。机器代码常常用 lead 来计算存储器引用的地址。

4. 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。

5. 指针也可以指向函数。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!