深入理解计算机系统-第三章-程序的机器级表示-3.1-3.3

3.2 程序编码

假设我们写一个C程序,有两个文件p1.c和p2.c,我们用unix命令行来编译这段代码
gcc -o2 -o p p1.c p2.c
o2:告诉编译器会使用第二级优化,会使得最终程序运行得更快,但是编译时间会变长,对代码进行调试会变得更加困难
这个命令实际上调用了一系列的程序,将源代码转化为可执行的代码

  1. C预处理器会扩展源代码,插入所有用#include命令指定的文件,并扩展所有的宏。
  2. 编译器会产生两个源代码的汇编代码,名字分别为p1.s和p2.s
  3. 汇编器会将汇编代码转化为二进制目标代码文件p1.o和p2.o
  4. 最后,链接器会将两个目标文件与时间标准unix库函数的代码合并。并产生最终的可执行文件

机器级代码

前面我们就说过,计算机系统使用了多种不同的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,有两种抽象特别重要:

①、第一种是将机器级程序的格式和行为定义为指令集体系结构(Instruction set architecture ,ISA),它定义了处理器状态、指令的格式,以及每条指令对状态的影响。大多数 ISA,包括 Intel IA32 和 x86-64,将程序的行为描述成好像每条指令是按顺序执行的,即一条指令结束后,下一条指令开始。处理器的硬件远比描述的精细复杂,它们并发的执行许多指令,但是可以采取措施保证整体行为与 ISA 指定的顺序执行完全一致。

②、第二种是机器程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。

在整个编译过程中,编译器会完成大部分工作,将把用 C 语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的基本指令,也就是汇编语言,汇编语言在被汇编器转化成机器语言,然后计算机去执行。汇编语言也就是具有更好的可读性的机器语言,所以能够理解汇编代码以及它与原始 C 代码的关系,是理解计算机如何执行程序的关键步骤。

我们在写 C 程序时,处理器的状态都是隐藏的,即我们编码不用去直接操作处理器。但是在汇编语言中,如下的几个处理器状态是可见的:

一、程序计数器(在 IA32 中通常称为 PC,用 %eip 表示):指示将要执行的下一条指令在存储器中的地址。

二、整数寄存器文件:包含8个命名的位置,可以存储一些地址或者整数的数据。有的用来记录某些重要的程序状态,有的则用来保存临时数据。

三、条件码寄存器:保存最近执行的算数或逻辑指令的状态信息,它们用来实现控制或数据流中的条件变化,比如用来实现 if 和 while 语句。

四、浮点寄存器:存储浮点数。

注意:C 语言提供的模型可以在存储器中声明和分配各种数据类型的对象。但是实际上机器代码则只是简单的将存储器看成是一个很大的、按字节寻址的数组。

汇编代码不区分有符号或者无符号整数,不区分各种类型的指针。甚至不区分指针和整数。

程序存储器包含程序的目标代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块

程序存储器是用虚拟地址来寻址的,在任意给定的时刻,只是有限的一部分虚拟地址是合法的

  • 代码实例
  • 如下这是一段 C 程序代码 hello.c:
#include 
 
int main()
{
    return sum(1,3);
}
int accum = 0;
int sum(int x,int y)
{
    int t= x+y;
    accum += t;
    return t;  
}

然后执行如下命令生成汇编程序

1
gcc -O1 -S hello.c
  -O1是优化选项,少优化->多优化:

O0 -->> O1 -->> O2 -->> O3

-O0表示没有优化,-O1为缺省值,-O3优化级别最高

生成的汇编程序 hello.s

        .file   "hello.c"
        .text
.globl sum
        .type   sum, @function  //定义全局函数sum
sum:
.LFB12:
        .cfi_startproc
        leal    (%rsi,%rdi), %eax //把寄存器%rsi和寄存器%rdi的值的地址装入eax中,即&(rsi+rdi)=eax
        addl    %eax, accum(%rip) //把寄存器%eax和寄存器%rip的值相加,并存放到 %rip中
        ret
        .cfi_endproc
.LFE12:
        .size   sum, .-sum
.globl main   //主函数main
        .type   main, @function
main:
.LFB11:
        .cfi_startproc
        movl    $3, %esi //将数据3复制到%esi寄存器
        movl    $1, %edi
        movl    $0, %eax
        call    sum  //将 sum 指令的地址压入到栈中,也就是下一条指令执行调用 sum 函数
        rep
        ret
        .cfi_endproc
.LFE11:
        .size   main, .-main
.globl accum  //定义全局变量accum
        .bss
        .align 4
        .type   accum, @object
        .size   accum, 4
accum:
        .zero   4
        .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-18)"
        .section        .note.GNU-stack,"",@progbits

注意:所有以 ‘.’ 开头的行都是指导汇编器和链接器的命令,我们通常可以忽略这些行。

现在这些汇编指令大家可以不用完全理解,后面会详细进行讲解。

使用
gcc -O2 -c code.c
指令会产生目标代码文件code.o,它是二进制格式的,所以无法直接读
深入理解计算机系统-第三章-程序的机器级表示-3.1-3.3_第1张图片

深入理解计算机系统-第三章-程序的机器级表示-3.1-3.3_第2张图片

  • 反汇编
  • 要查看目标代码文件的内容,有一类称为反汇编器
  • 深入理解计算机系统-第三章-程序的机器级表示-3.1-3.3_第3张图片
    左边被分成了一些组,每组有1-6个字节。每组都是一套指令,右边是等价的汇编语言,其中的一些特性
  • IA32指令长度从1-15个字节不等,常用的指令所需的字节少,不太常用的字节多
  • 反汇编器只是根据目标文件的字节序列来确定汇编代码的,不需要访问程序的源代码或者汇编

数据格式

由于计算机是由16位体系结构扩展为32位体系结构的,Intel 用术语 “字”(word) 表示16位数据类型,因此 32 位表示 “双字”(double words),64 位数称为“四字”(quad words).

前面的汇编代码我们可以看到所有的汇编指令都带有字母 l,比如movl、addl、subl、pushl等等,这个l的后缀其实就是表示的数据格式,表示我们操作的是32位的数值。

下面我们看一下 C 语言基本数据类型对应的 IA32 表示:

深入理解计算机系统-第三章-程序的机器级表示-3.1-3.3_第4张图片
  上面的图示很好理解,比如mov指令,它是一个数据传送的指令,那么movb就代表传送一个字节的数据,movw就代表传送两个字节的数据,而movl就代表传送四个字节的数据。需要注意的是,long long int在IA32架构中是不支持这种数据格式的。而且汇编代码使用后缀 “l” 来表示 4 字节整数和8字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

你可能感兴趣的:(深入理解计算机系统)