【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)

简介: 【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
前言:看过很多书,但总是忘得很快。知识广度越大越容易接纳新东西,但从考察角度来说,自然是对某个方面了解越深越好。那些大而全的著作虽然每本都是经典中的经典,但实际工作中可能只用到其中的一小部分。我现在越发觉得少即是多,看再多东西没有理解透彻都是白搭,把最常用的每天过一遍才是最有效的。
  1. 谈谈Linux内核的学习路线,具体该怎么学
  2. 耗时半月,终于把牛客网上的Linux内核进阶成神笔记整理好了
  3. 牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万

image.png

作系统概念->王道操作系统考研书->操作系统导论->深入理解计算机系统->汇编/保护模式汇编/gcc内嵌汇编->哈工大李志军老师的操作系统慕课及实验->linux内核设计及实现->linux命令行与shell编程->linux内核完全剖析->unix环境高级编程->unix网络编程->Linux/unix系统编程上下册->linux技术内幕(非常重量级,是基于内核3.2的,结合全部源码看最好)


这一套我大概花了2年,比较费劲,但是学完了能将计算机基础课中所有的知识点串起来,会有种悟道的感觉,有空闲时间的同学非常建议去学一学。学完这一套,只要不是面大厂的操作系统相关核心岗位,在操作系统与网络这一部分基本上不会有啥压力。就是比较废时间,加油呀!


【考研必备一】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)


5、指令系统


指令系统是计组中的一单元,知识点不少,计划用3篇文章解释清楚指令系统的知识。


第一节 指令格式


指令系统主要分三大内容

指令系统:一台计算机中所有指令的集合。


发展:复杂指令系统-精简指令系统


复杂指令系统计算机(CISC):为增强指令系统的功能,设置一些功能复杂的指令,把一些原来由软件实现,常用功能改用硬件的指令系统来实现。目前绝大多数计算机都属于这。


精简指令系统(RISC):尽量简化指令系统,只保留那些功能简单,能在一个节拍内执行完的指令,较复杂的功能用一段子程序来实现。


指令格式


指令格式包括两个方面:操作码和地址码


操作码(OP):表示该指令应该进行什么性质的操作,如加减乘除,存数,取数等。


地址码:


指令字长度


1.定义:一个指令字包含二进制代码的位数。


2.机器字长:计算机能直接处理的二进制数据的位数。


指令系统


1.指令集体系结构:一个指令器支持的指令和指令的字节级编码。


不同的处理器族支持不同的指令集体系结构,因此,一个程序被编译在一种机器上运行,往往不能在另一种机器上运行。


2.按暂存机制分类,根据在CPU内部存储操作数的区别,可以吧指令集系统分为3类:堆栈(Stack),累加器,和寄存器组。


指令的流水处理


1.指令控制方式:顺序,重叠和流水。


(1)顺序方式:各条机器指令之间顺序串行的执行,执行完一条后才取下一条指令,而且每条指令内部的微操作也是顺序执行的。


(2)重叠方式:在解释第K条指令操作完成之前就开始解释第K+1条指令。但容易冲突。


(3)流水方式:仿流水线作业,并行处理。同时解释多条指令。


指令类型


日常使用的 Intel CPU 大概有 2000 多条 CPU 指令。可以分为以下 5 大类型:


  • 算术类指令:加减乘除。
  • 数据传输类指令:变量赋值、读写内存数据。
  • 逻辑类指令:与或非。
  • 条件分支类指令:条件判断语句。
  • 无条件跳转指令:方法、函数的调用跳转。

继续细分的话,具有如下指令类型


  • 算术逻辑运算指令
  • 移位操作指令
  • 算术移位
  • 逻辑移位
  • 循环移位
  • 矢量运算指令(矩阵运算)
  • 浮点运算指令
  • 十进制运算指令
  • 字符串处理指令
  • 字符串传送
  • 字符串比较
  • 字符串查询
  • 字符串转换
  • 数据传输指令
  • 寄存器与寄存器传输
  • 寄存器与主存储器单元传输
  • 存储器单元与存储器单元传输
  • 数据交换(源操作数与目的操作下互换)
  • 转移指令
  • 条件转移
  • 无条件转移
  • 过程调用与返回
  • 陷阱
  • 堆栈及堆栈操作指令
  • I/O 指令
  • 特权指令
  • 多处理机指令(在多处理器系统中保证共享数据的一致性等)
  • 控制指令


第二节 寻址方式(重点)


指令寻址


指令寻址,即是根据指令字的地址码来获取到实际的数据,寻址的方式跟硬件关系密切,不同的计算机有不同的寻址方式。有的计算机寻址方式种类少,所以会直接在操作码上表示寻址方式;有些计算机的寻址方式种类多,就会在指令字中添加一个特别用于标记寻址方式的字段,例如:假设该字段具有 3 位,那么就可以表示 8 种寻址方式。


NOTE:寻址方式与 CPU 内的寄存器设计密切相关。


直接寻址:指令字的地址码直接给出了操作数在存储器中的地址,是最简单的寻址方式。


间接寻址:指令字的地址码所指向的寄存器或存储器的内容并不是真实的操作数,而是操作数的地址。间接寻址常用于跳转指令,只要修改寄存器或存储器的地址就可以实现跳转到不同的操作数上。


相对寻址:把程序计数器(PC)的内容,即当前执行指令的地址与地址码部分给出的偏移量(Disp)之和作为操作数的地址。这种寻址方式同样常用于跳转(转移)指令,当程序执行到本条指令后,跳转到 PC+Disp。


立即数寻址:即地址码本身就是一个操作数的寻址方式,该方式的特点就是数据块(因为实际上没有寻址),但操作数固定。常用于为某个寄存器或存储器单元赋初值,或提供一个常数。


通用寄存器寻址:CPU 中大概会有几个到几十个通用寄存器用于临时储存操作数、操作数的地址或中间结果,指令字的地址码可以指向这些寄存器。通用寄存器具有地址短,存取速度快的特性,所以地址码指向通用寄存器的指令的长度也会更短,节省存储空间,执行效率更快。常被用于执行速度要求严格的指令中。


基址寄存器寻址:基址,即基础地址,基址寄存器就是存放基址的寄存器,可以是一个专用寄存器,也可以使用通用寄存器来充当基址寄存器。执行指令时,需要将基址与指令字的地址码结合得到完成的地址,此时的地址码充当着偏移量(位移量)的角色。当存储器容量较大时,直接寻址方式是无法存取到所有存储单元的,所以通常会采用 分段分页 的内存管理方式。此时,段或页的首地址就会存放于基址寄存器中,而指令字的地址码就作为段或页的长度,这样只要修改基址寄存器的内容就可以访问到存储器的任意单元了。这种寻址方式常被用于为程序或数据分配存储区,与虚拟地址实现密切相关。基址寄存器寻址方式解决了程序在存储器中的定位存储单元和扩大 CPU 寻址空间的问题。


变址寄存器寻址:变址寄存器内的地址与指令字地址之和得到了实际的有效地址,如果 CPU 中存在基址寄存器,那么就还得加上基址地址。这种寻址方式常用于处理需要循环执行的程序,例如:循环处理数组,此时变址寄存器所改变的就是数组的下标了。


堆栈寻址:堆栈是有若干个连续的存储器单元组成的先进后出(FILO)存储区。堆栈是用于提供操作数和保存运算结果的主要存储区,同时还主要用于暂存中断和子程序调用时的线程数据及返回地址。


通过 MIPS 感受指令字的设计


MIPS(Millions of Instructions Per Second)是一种最简单的精简指令集架构,由 MIPS 科技公司设计。MIPS 指令具有 32 位(最新版本为 64 位),高 6 位为操作码(OPCODE),描述了指令的操作类型。其余 26 位具有 3 种格式:R、I 和 J。不同的指令类型和操作码组合能够完成多种功能实现,如下:


加法算数指令add $t0,$s2,$s1的指令字及其对应的机器码如下:


640.jpg

  • opcode:0
  • rs:代表第一个寄存器 s1 的地址是 17
  • rt:代表第二个寄存器 s2 的地址是 18
  • rd:代表目标临时寄存器 t0 的地址是 8
  • shamt:0,表示不位移


最终加法算数指令 add $t0,$s2,$s1 的二进制机器码表示为 000000 10001 10010 01000 00000 1000000(0X02324020)。可以看见,机器码中没有保存任何实际的程序数据,而是保存了程序数据的储存的地址,这也算是存储程序计算机指令集设计的一大特点。


将高级语言翻译成汇编代码


为什么要保留汇编语言


汇编语言是与机器语言最接近的高级编程语言(或称为中级编程语言),汇编语言基本上与机器语言对应,即汇编指令和计算机指令是相对匹配的。虽然汇编语言具有与硬件的关系密切,占用内存小,运行速度快等优点,但也具有可读性低、可重用性差,开发效率低下等问题。高级语言的出现是为了解决这些问题,让软件开发变得更加简单高效,易于协作。但高级语言也存在自己的缺陷,例如:难以编写直接操作硬件设备的程序等。


所以为了权衡上述的问题,最终汇编语言被作为中间的状态保留了下来。一些高级语言(e.g. C 语言)提供了与汇编语言之间的调用接口,汇编程序可作为高级语言的外部过程或函数,利用堆栈在两者之间传递参数或参数的访问地址。两者的源程序通过编译或汇编生成目标文件(OBJ)之后再利用连接程序(linker)把它们连接成为可执行文件便可在计算机上运行了。保留汇编语言还为程序员提供一种调优的手段,无论是 C 程序还是 Python 程序,当我们要进行代码性能优化时,了解程序的汇编代码是一个不错的切入点。


顺序程序流


计算机指令是一种逻辑上的抽象设计,而机器码则是计算机指令的物理表现。机器码(Machine Code),又称为机器语言,本质是由 0 和 1 组成的数字序列。一条机器码就是一条计算机指令。程序由指令组成,但让人类使用机器码来编写程序显然是不人道的,所以逐步发展了对人类更加友好的高级编程语言。这里我们需要了解计算机是如何将高级编程语言编译为机器码的。


Step 1. 编写高级语言程序。


// test.c
int main()
{
  int a = 1;
  int b = 2;
  a = a + b;
}

Step 2. 编译(Compile),将高级语言编译成汇编语言(ASM)程序。


$ gcc -g -c test.c

Step 3. 使用 objdump 命令反汇编目标文件,输出可阅读的二进制信息。下述左侧的一堆数字序列就是一条条机器码,右侧 push、mov、add、pop 一类的就是汇编代码。


$ objdump -d -M intel -S test.o
test.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
int main()
{
   0:    55                       push   rbp
   1:    48 89 e5                 mov    rbp,rsp
  int a = 1;
   4:    c7 45 fc 01 00 00 00     mov    DWORD PTR [rbp-0x4],0x1
  int b = 2;
   b:    c7 45 f8 02 00 00 00     mov    DWORD PTR [rbp-0x8],0x2
  a = a + b;
  12:    8b 45 f8                 mov    eax,DWORD PTR [rbp-0x8]
  15:    01 45 fc                 add    DWORD PTR [rbp-0x4],eax
}
  18:    5d                       pop    rbp
  19:    c3                       ret

NOTE:这里的程序入口是main()函数,而不是第 0 条汇编代码。


条件程序流


值得注意的是,某些特殊的指令,比如跳转指令,会主动修改 PC 的内容,此时下一条地址就不是从存储器中顺序加载的了,而是到特定的位置加载指令内容。这就是 if…else 条件语句,while/for 循环语句的底层支撑原理。


Step 1. 编写高级语言程序。


// test.c
#include <time.h>
#include <stdlib.h>
int main()
{
  srand(time(NULL));
  int r = rand() % 2;
  int a = 10;
  if (r == 0)
  {
    a = 1;
  } else {
    a = 2;
  }
}

Step 2. 编译(Compile),将高级语言编译成汇编语言。

$ gcc -g -c test.c

Step 3. 使用 objdump 命令反汇编目标文件,输出可阅读的二进制信息。我们主要分析 if…else 语句。

if (r == 0)
  33:    83 7d fc 00              cmp    DWORD PTR [rbp-0x4],0x0
  37:    75 09                    jne    42 <main+0x42>
  {
    a = 1;
  39:    c7 45 f8 01 00 00 00     mov    DWORD PTR [rbp-0x8],0x1
  40:    eb 07                    jmp    49 <main+0x49>
  } else {
    a = 2;
  42:    c7 45 f8 02 00 00 00     mov    DWORD PTR [rbp-0x8],0x2
  }

首先进入条件判断,汇编代码为 cmp 比较指令,比较数 1:DWORD PTR [rbp-0x4] 表示变量 r 是一个 32 位整数,数据在寄存器 [rbp-0x4] 中;比较数 2:0x0 表示常量 0 的十六进制。比较的结果会存入到 条件码寄存器,等待被其他指令读取。当判断条件为 True 时,ZF 设置为 1,反正设置为 0。


条件码寄存器(Condition Code)是一种单个位寄存器,它们的值只能为 0 或者 1。当有算术与逻辑操作发生时,这些条件码寄存器当中的值就随之发生变化。后续的指令通过检测这些条件码寄存器来执行条件分支指令。常用的条件码类型如下:


  • CF:进位标志寄存器。最近的操作是最高位产生了进位。它可以记录无符号操作的溢出,当溢出时会被设为 1。
  • ZF:零标志寄存器,最近的操作得出的结果为 0。当计算结果为 0 时将会被设为 1。
  • SF:符号标志寄存器,最近的操作得到的结果为负数。当计算结果为负数时会被设为 1。
  • OF:溢出标志寄存器,最近的操作导致一个补码溢出(正溢出或负溢出)。当计算结果导致了补码溢出时,会被设为 1。

回到正题,PC 继续自增,执行下一条 jnp 指令。jnp(jump if not equal)会查看 ZF 的内容,若为 0 则跳转到地址 42 (42 表示汇编代码的行号)。前文提到,当 CPU 执行跳转类指令时,PC 就不再通过自增的方式来获得下一条指令的地址,而是直接被设置了 42 行对应的地址。由此,CPU 会继续将 42 对应的指令读取到 IR 中并执行下去。


42 行执行的是 mov 指令,表示将操作数 2:0x2 移入到 操作数 1:DWORD PTR [rbp-0x8] 中。就是一个赋值语句的底层实现支撑。接下来 PC 恢复如常,继续以自增的方式获取下一条指令的地址。


循环程序流

  • C 语言代码
// test.c
int main()
{
    int a = 0;
    int i;
    for (i = 0; i < 3; i++)
    {
        a += i;
    }
}
  • 计算机指令与汇编代码
for (i = 0; i < 3; i++)
   b:    c7 45 f8 00 00 00 00     mov    DWORD PTR [rbp-0x8],0x0
  12:    eb 0a                    jmp    1e <main+0x1e>
    {
        a += i;
  14:    8b 45 f8                 mov    eax,DWORD PTR [rbp-0x8]
  17:    01 45 fc                 add    DWORD PTR [rbp-0x4],eax
    for (i = 0; i < 3; i++)
  1a:    83 45 f8 01              add    DWORD PTR [rbp-0x8],0x1
  1e:    83 7d f8 02              cmp    DWORD PTR [rbp-0x8],0x2
  22:    7e f0                    jle    14 <main+0x14>
    }

函数调用栈的工作原理


与普通的跳转程序(e.g. if…else、while/for)不同,函数调用的特点在于具有回归(return)的特点,在调用的函数执行完之后会再次回到执行调用的 call 指令的位置,继续往下执行。能够实现这个效果,完全依赖堆栈(Stack)存储区的特性。 首先我们需要了解几个概念。


  • 堆栈(Stack):是有若干个连续的存储器单元组成的先进后出(FILO)存储区,用于提供操作数、保存运算结果、暂存中断和子程序调用时的线程数据及返回地址。通过执行堆栈的 Push(压栈)和 Pop(出栈)操作可以将指定的数据在堆栈中放入和取出。堆栈具有栈顶和栈底之分,栈顶的地址最低,而栈底的地址最高。堆栈的 FILO 的特性非常适用于函数调用的场景:父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。
  • 栈帧(Stack Frame):是堆栈中的逻辑空间,每次函数调用都会在堆栈中生成一个栈帧,对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境,保存了函数的参数、函数的局部变量以及函数执行完后返回到哪里的返回地址等等。栈帧的本质是两个指针寄存器:EBP(基址指针,又称帧指针)和 ESP(栈指针)。其中 EBP 指向帧底,而 ESP 指向栈顶。当程序运行时,ESP 是可以移动的,大多数信息的访问都通过移动 ESP 来完成,而 EBP 会一直处于帧低。EBP ~ ESP 之间的地址空间,就是当前执行函数的地址空间。


NOTE:EBP 指向当前位于系统栈最上边一个栈帧的底部,而不是指向系统栈的底部。严格说来,“栈帧底部” 和 “系统栈底部” 不是同一个概念,而 ESP 所指的栈帧顶部和系统栈顶部是同一个位置。

简单概括一下函数调用的堆栈行为,ESP 随着当前函数的压栈和出栈会不断的移动,但由于 EBP 的存在,所以当前执行函数栈帧的边界是始终清晰的。当一个当前的子函数调用完成之后,EBP 就会跳到父函数栈帧的底部,而 ESP 也会随其自然的来到父函数栈帧的头部。所以,理解函数调用堆栈的运作原理,主要要掌握 EBP 和 ESP 的动向。下面以一个例子来说明。


NOTE:我们习惯将将父函数(调用函数的函数)称为 “调用者(Caller)”,将子函数(被调用的函数)称为 “被调用者(Callee)”。

  • C 程序代码
#include <stdio.h>
int add(int a, int b) {
    int result = 0;
    result = a + b;
    return result;
}
int main(int argc, char *argv[]) {
    int result = 0;
    result = add(1, 2);
    printf("result = %d \r\n", result);
    return 0;
}
  • 使用gcc编译,然后gdb反汇编main函数,看看它是如何调用add函数的
(gdb) disassemble main 
Dump of assembler code for function main:
   0x08048439 <+0>:     push   %ebp
   0x0804843a <+1>:     mov    %esp,%ebp
   0x0804843c <+3>:     and    $0xfffffff0,%esp
   0x0804843f <+6>:     sub    $0x20,%esp
   0x08048442 <+9>:     movl   $0x0,0x1c(%esp)  # 给 result 变量赋 0 值
   0x0804844a <+17>:    movl   $0x2,0x4(%esp)   # 将第 2 个参数 argv 压栈(该参数偏移为esp+0x04)
   0x08048452 <+25>:    movl   $0x1,(%esp)      # 将第 1 个参数 argc 压栈(该参数偏移为esp+0x00)
   0x08048459 <+32>:    call   0x804841c <add>  # 调用 add 函数
   0x0804845e <+37>:    mov    %eax,0x1c(%esp)  # 将 add 函数的返回值地址赋给 result 变量,作为子函数调用完之后的回归点
   0x08048462 <+41>:    mov    0x1c(%esp),%eax
   0x08048466 <+45>:    mov    %eax,0x4(%esp)
   0x0804846a <+49>:    movl   $0x8048510,(%esp)
   0x08048471 <+56>:    call   0x80482f0 <printf@plt>
   0x08048476 <+61>:    mov    $0x0,%eax
   0x0804847b <+66>:    leave  
   0x0804847c <+67>:    ret    
End of assembler dump.
(gdb) disassemble add
Dump of assembler code for function add:
   0x0804841c <+0>:     push   %ebp             # 将 ebp 压栈(保存函数调用者的栈帧基址)
   0x0804841d <+1>:     mov    %esp,%ebp        # 将 ebp 指向栈顶 esp(设置当前被调用函数的栈帧基址)
   0x0804841f <+3>:     sub    $0x10,%esp       # 分配栈空间(栈向低地址方向生长)
   0x08048422 <+6>:     movl   $0x0,-0x4(%ebp)  # 给 result 变量赋 0 值(该变量偏移为ebp-0x04)
   0x08048429 <+13>:    mov    0xc(%ebp),%eax   # 将第 2 个参数的值赋给 eax 寄存器(准备运算)
   0x0804842c <+16>:    mov    0x8(%ebp),%edx   # 将第 1 个参数的值赋给 edx 寄存器(准备运算)
   0x0804842f <+19>:    add    %edx,%eax        # 运算器执行加法运算 (edx+eax),结果保存在 eax 寄存器中
   0x08048431 <+21>:    mov    %eax,-0x4(%ebp)  # 将运算结果 eax 赋给 result 变量
   0x08048434 <+24>:    mov    -0x4(%ebp),%eax  # 将 result 变量的值赋给 eax 寄存器(eax 的地址将作为函数返回值)
   0x08048437 <+27>:    leave                   # 恢复函数调用者的栈帧基址(pop %ebp)
   0x08048438 <+28>:    ret                     # 返回(准备执行下条指令)
End of assembler dump.

可见,每一次函数调用,都会对调用者的栈帧基址 EBP 进行压栈操作(为了调用回归),并且由于子函数的栈帧基址 EBP 来自于栈指针 ESP 而来(生成新的子函数的栈帧),所以各层函数的栈帧基址很巧妙的构成了一个链,即当前的栈帧基址指向下一层函数栈帧基址所在的位置。


由此当子函数执行完成时,ESP 依旧在栈顶,但 EBP 就跳转到父函数的栈帧底部了,并且堆栈下一个弹出的就是子函数的调用回归点,最终程序流回到调用点并继续往下执行。


通过函数调用堆栈的工作原理我们可以看出,无论程序中具有多少层的函数调用,或递归调用,只需要维护好每个栈帧的 EBP 和 ESP 就可以管理还函数之间的跳转。但堆栈也是由容量限制的,如果函数调用的层级太多就会出现栈溢出的错误(Stack Overflow)。


程序在操作系统中的装载与运行


一个程序在操作系统上运行需要经历以下阶段:

第一阶段:得到可执行文件

  1. 编译(Compile)
  2. 汇编(Assemble)
  3. 链接(Link)

第二阶段:装载运行

  1. 装载器(Loader)将可执行文件载入到内存
  2. CPU 从内存中可执行文件的程序入口开始读取指令和数据,开始真正执行程序。

编译和汇编的过程在上文中已经提到了,下面再继续介绍链接的过程。

  • 子程序
// add_lib.c
int add(int a, int b)
{
    return a+b;
}
  • 主函数
// link_example.c
#include <stdio.h>
int main()
{
    int a = 10;
    int b = 5;
    int c = add(a, b);
    printf("c = %d\n", c);
}
  • 编译 C 程序得到 Object 文件
$ gcc -g -c add_lib.c link_example.c
  • 链接上述两个 Object 文件得到一个可执行文件
$ gcc -o link-example add_lib.o link_example.o
$ ./link-example
c = 15

区别于 Object 文件,真正的可执行文件的内容如下:

$ objdump -d -M intel -S link-example
link-example:     file format elf64-x86-64
Disassembly of section .init:
00000000004003c8 <_init>:
  4003c8:    48 83 ec 08              sub    rsp,0x8
  4003cc:    48 8b 05 25 0c 20 00     mov    rax,QWORD PTR [rip+0x200c25]        # 600ff8 <__gmon_start__>
  4003d3:    48 85 c0                 test   rax,rax
  4003d6:    74 05                    je     4003dd <_init+0x15>
  4003d8:    e8 43 00 00 00           call   400420 <.plt.got>
  4003dd:    48 83 c4 08              add    rsp,0x8
  4003e1:    c3                       ret
Disassembly of section .plt:
00000000004003f0 <.plt>:
  4003f0:    ff 35 12 0c 20 00        push   QWORD PTR [rip+0x200c12]        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003f6:    ff 25 14 0c 20 00        jmp    QWORD PTR [rip+0x200c14]        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003fc:    0f 1f 40 00              nop    DWORD PTR [rax+0x0]
0000000000400400 <printf@plt>:
  400400:    ff 25 12 0c 20 00        jmp    QWORD PTR [rip+0x200c12]        # 601018 <printf@GLIBC_2.2.5>
  400406:    68 00 00 00 00           push   0x0
  40040b:    e9 e0 ff ff ff           jmp    4003f0 <.plt>
0000000000400410 <__libc_start_main@plt>:
  400410:    ff 25 0a 0c 20 00        jmp    QWORD PTR [rip+0x200c0a]        # 601020 <__libc_start_main@GLIBC_2.2.5>
  400416:    68 01 00 00 00           push   0x1
  40041b:    e9 d0 ff ff ff           jmp    4003f0 <.plt>
Disassembly of section .plt.got:
0000000000400420 <.plt.got>:
  400420:    ff 25 d2 0b 20 00        jmp    QWORD PTR [rip+0x200bd2]        # 600ff8 <__gmon_start__>
  400426:    66 90                    xchg   ax,ax
Disassembly of section .text:
0000000000400430 <_start>:
  400430:    31 ed                    xor    ebp,ebp
  400432:    49 89 d1                 mov    r9,rdx
  400435:    5e                       pop    rsi
  400436:    48 89 e2                 mov    rdx,rsp
  400439:    48 83 e4 f0              and    rsp,0xfffffffffffffff0
  40043d:    50                       push   rax
  40043e:    54                       push   rsp
  40043f:    49 c7 c0 f0 05 40 00     mov    r8,0x4005f0
  400446:    48 c7 c1 80 05 40 00     mov    rcx,0x400580
  40044d:    48 c7 c7 31 05 40 00     mov    rdi,0x400531
  400454:    e8 b7 ff ff ff           call   400410 <__libc_start_main@plt>
  400459:    f4                       hlt
  40045a:    66 0f 1f 44 00 00        nop    WORD PTR [rax+rax*1+0x0]
0000000000400460 <deregister_tm_clones>:
  400460:    b8 37 10 60 00           mov    eax,0x601037
  400465:    55                       push   rbp
  400466:    48 2d 30 10 60 00        sub    rax,0x601030
  40046c:    48 83 f8 0e              cmp    rax,0xe
  400470:    48 89 e5                 mov    rbp,rsp
  400473:    77 02                    ja     400477 <deregister_tm_clones+0x17>
  400475:    5d                       pop    rbp
  400476:    c3                       ret
  400477:    b8 00 00 00 00           mov    eax,0x0
  40047c:    48 85 c0                 test   rax,rax
  40047f:    74 f4                    je     400475 <deregister_tm_clones+0x15>
  400481:    5d                       pop    rbp
  400482:    bf 30 10 60 00           mov    edi,0x601030
  400487:    ff e0                    jmp    rax
  400489:    0f 1f 80 00 00 00 00     nop    DWORD PTR [rax+0x0]
0000000000400490 <register_tm_clones>:
  400490:    b8 30 10 60 00           mov    eax,0x601030
  400495:    55                       push   rbp
  400496:    48 2d 30 10 60 00        sub    rax,0x601030
  40049c:    48 c1 f8 03              sar    rax,0x3
  4004a0:    48 89 e5                 mov    rbp,rsp
  4004a3:    48 89 c2                 mov    rdx,rax
  4004a6:    48 c1 ea 3f              shr    rdx,0x3f
  4004aa:    48 01 d0                 add    rax,rdx
  4004ad:    48 d1 f8                 sar    rax,1
  4004b0:    75 02                    jne    4004b4 <register_tm_clones+0x24>
  4004b2:    5d                       pop    rbp
  4004b3:    c3                       ret
  4004b4:    ba 00 00 00 00           mov    edx,0x0
  4004b9:    48 85 d2                 test   rdx,rdx
  4004bc:    74 f4                    je     4004b2 <register_tm_clones+0x22>
  4004be:    5d                       pop    rbp
  4004bf:    48 89 c6                 mov    rsi,rax
  4004c2:    bf 30 10 60 00           mov    edi,0x601030
  4004c7:    ff e2                    jmp    rdx
  4004c9:    0f 1f 80 00 00 00 00     nop    DWORD PTR [rax+0x0]
00000000004004d0 <__do_global_dtors_aux>:
  4004d0:    80 3d 55 0b 20 00 00     cmp    BYTE PTR [rip+0x200b55],0x0        # 60102c <_edata>
  4004d7:    75 11                    jne    4004ea <__do_global_dtors_aux+0x1a>
  4004d9:    55                       push   rbp
  4004da:    48 89 e5                 mov    rbp,rsp
  4004dd:    e8 7e ff ff ff           call   400460 <deregister_tm_clones>
  4004e2:    5d                       pop    rbp
  4004e3:    c6 05 42 0b 20 00 01     mov    BYTE PTR [rip+0x200b42],0x1        # 60102c <_edata>
  4004ea:    f3 c3                    repz ret
  4004ec:    0f 1f 40 00              nop    DWORD PTR [rax+0x0]
00000000004004f0 <frame_dummy>:
  4004f0:    48 83 3d 28 09 20 00     cmp    QWORD PTR [rip+0x200928],0x0        # 600e20 <__JCR_END__>
  4004f7:    00
  4004f8:    74 1e                    je     400518 <frame_dummy+0x28>
  4004fa:    b8 00 00 00 00           mov    eax,0x0
  4004ff:    48 85 c0                 test   rax,rax
  400502:    74 14                    je     400518 <frame_dummy+0x28>
  400504:    55                       push   rbp
  400505:    bf 20 0e 60 00           mov    edi,0x600e20
  40050a:    48 89 e5                 mov    rbp,rsp
  40050d:    ff d0                    call   rax
  40050f:    5d                       pop    rbp
  400510:    e9 7b ff ff ff           jmp    400490 <register_tm_clones>
  400515:    0f 1f 00                 nop    DWORD PTR [rax]
  400518:    e9 73 ff ff ff           jmp    400490 <register_tm_clones>
000000000040051d <add>:
// add_lib.c
int add(int a, int b)
{
  40051d:    55                       push   rbp
  40051e:    48 89 e5                 mov    rbp,rsp
  400521:    89 7d fc                 mov    DWORD PTR [rbp-0x4],edi
  400524:    89 75 f8                 mov    DWORD PTR [rbp-0x8],esi
    return a+b;
  400527:    8b 45 f8                 mov    eax,DWORD PTR [rbp-0x8]
  40052a:    8b 55 fc                 mov    edx,DWORD PTR [rbp-0x4]
  40052d:    01 d0                    add    eax,edx
}
  40052f:    5d                       pop    rbp
  400530:    c3                       ret
0000000000400531 <main>:
// link_example.c
#include <stdio.h>
int main()
{
  400531:    55                       push   rbp
  400532:    48 89 e5                 mov    rbp,rsp
  400535:    48 83 ec 10              sub    rsp,0x10
    int a = 10;
  400539:    c7 45 fc 0a 00 00 00     mov    DWORD PTR [rbp-0x4],0xa
    int b = 5;
  400540:    c7 45 f8 05 00 00 00     mov    DWORD PTR [rbp-0x8],0x5
    int c = add(a, b);
  400547:    8b 55 f8                 mov    edx,DWORD PTR [rbp-0x8]
  40054a:    8b 45 fc                 mov    eax,DWORD PTR [rbp-0x4]
  40054d:    89 d6                    mov    esi,edx
  40054f:    89 c7                    mov    edi,eax
  400551:    b8 00 00 00 00           mov    eax,0x0
  400556:    e8 c2 ff ff ff           call   40051d <add>
  40055b:    89 45 f4                 mov    DWORD PTR [rbp-0xc],eax
    printf("c = %d\n", c);
  40055e:    8b 45 f4                 mov    eax,DWORD PTR [rbp-0xc]
  400561:    89 c6                    mov    esi,eax
  400563:    bf 10 06 40 00           mov    edi,0x400610
  400568:    b8 00 00 00 00           mov    eax,0x0
  40056d:    e8 8e fe ff ff           call   400400 <printf@plt>
}
  400572:    c9                       leave
  400573:    c3                       ret
  400574:    66 2e 0f 1f 84 00 00     nop    WORD PTR cs:[rax+rax*1+0x0]
  40057b:    00 00 00
  40057e:    66 90                    xchg   ax,ax
0000000000400580 <__libc_csu_init>:
  400580:    41 57                    push   r15
  400582:    41 89 ff                 mov    r15d,edi
  400585:    41 56                    push   r14
  400587:    49 89 f6                 mov    r14,rsi
  40058a:    41 55                    push   r13
  40058c:    49 89 d5                 mov    r13,rdx
  40058f:    41 54                    push   r12
  400591:    4c 8d 25 78 08 20 00     lea    r12,[rip+0x200878]        # 600e10 <__frame_dummy_init_array_entry>
  400598:    55                       push   rbp
  400599:    48 8d 2d 78 08 20 00     lea    rbp,[rip+0x200878]        # 600e18 <__init_array_end>
  4005a0:    53                       push   rbx
  4005a1:    4c 29 e5                 sub    rbp,r12
  4005a4:    31 db                    xor    ebx,ebx
  4005a6:    48 c1 fd 03              sar    rbp,0x3
  4005aa:    48 83 ec 08              sub    rsp,0x8
  4005ae:    e8 15 fe ff ff           call   4003c8 <_init>
  4005b3:    48 85 ed                 test   rbp,rbp
  4005b6:    74 1e                    je     4005d6 <__libc_csu_init+0x56>
  4005b8:    0f 1f 84 00 00 00 00     nop    DWORD PTR [rax+rax*1+0x0]
  4005bf:    00
  4005c0:    4c 89 ea                 mov    rdx,r13
  4005c3:    4c 89 f6                 mov    rsi,r14
  4005c6:    44 89 ff                 mov    edi,r15d
  4005c9:    41 ff 14 dc              call   QWORD PTR [r12+rbx*8]
  4005cd:    48 83 c3 01              add    rbx,0x1
  4005d1:    48 39 eb                 cmp    rbx,rbp
  4005d4:    75 ea                    jne    4005c0 <__libc_csu_init+0x40>
  4005d6:    48 83 c4 08              add    rsp,0x8
  4005da:    5b                       pop    rbx
  4005db:    5d                       pop    rbp
  4005dc:    41 5c                    pop    r12
  4005de:    41 5d                    pop    r13
  4005e0:    41 5e                    pop    r14
  4005e2:    41 5f                    pop    r15
  4005e4:    c3                       ret
  4005e5:    90                       nop
  4005e6:    66 2e 0f 1f 84 00 00     nop    WORD PTR cs:[rax+rax*1+0x0]
  4005ed:    00 00 00
00000000004005f0 <__libc_csu_fini>:
  4005f0:    f3 c3                    repz ret
Disassembly of section .fini:
00000000004005f4 <_fini>:
  4005f4:    48 83 ec 08              sub    rsp,0x8
  4005f8:    48 83 c4 08              add    rsp,0x8
  4005fc:    c3                       ret

可见,链接(Link) 不仅仅是单纯的将多个 Object 文件拼凑起来而已,而是将程序真正的转换为一个可以在操作系统上执行的文件格式,且这个文件中还包含了整个程序所有 Object 文件的内容。在 Linux 上,这个文件格式就是 ELF(Execuatable and Linkable File Format,可执行与可链接文件格式)。


ELF 文件格式:是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。ELF 文件由 4 部分组成,分别是 ELF header、程序头表(Program Header Table)、节(Section)和节头表(Section Header Table)。


在链接器把程序转换为 ELF 格式的可执行文件之后,装载器再去处理就会容易得多。因为装载器不再需要考虑地址跳转的问题,只需要解析 ELF 文件,把对应的指令和数据加载到内存里面供 CPU 执行就可以了。


同样,Windows 也有自己的可执行文件格式 PE(Portable Executable Format)。因为 Linux 和 Windows 的可执行文件格式不同,所以也就不能够 “一次编译,跨平台执行” 了。那么换句话说:是不是只要在 Linux 上运行可以解析 PE 文件的装载器就可以解决这个问题呢?答案是肯定的,Linux 著名的开源软件 Wine 正是此类装载器,国内很多 Linux Desktop 发行版都是基于 Wine 实现了 Windows 常用软件的移植


第三节 高级语言程序与机器级代码之间的对应*


机器语言:计算机执行的二进制命令,都是0和1表示的


汇编语言:用助记符代替机器指令的操作码(如:ADD表示加法)


高级语言:更简单,符合人们的习惯,也更容易理解和修改。高级语言经过编译器编译之后可以得到目标程序。(如:C++、JAVA)


机器语言、汇编语言、高级语言的区别:依次接近人类自然语言的表达方式、代码效率依次变低、语言越来越高级


机器语言、汇编语言、高级语言的联系:高级语言要通过编译程序翻译成汇编代码,汇编语言要通过汇编得到机器语言,计算机才能执行。


一、基本概念

  • 对操作数的操作地址只涉及三种:寄存器到寄存器,主存到寄存器,立即数到寄存器。
  • dword 32bit ;word 16bit ;byte 8bit
  • 通用寄存器 eax ebx ecx edx 变址寄存器 esi edi 堆栈寄存器 ebp esp。

二、选择结构语句的机器级表示

je jump when equal,
jne jump when not equal,
jg jump when greater,
jge jump when greater or equal
jl jump when less
jle jump when less or equal

例如

cmp eax ,ebx #比较寄存器eax和ebx里的值jg NEXT #若eax>ebx,则跳转到NEXT


三、循环结构语句的机器级表示

用条件指令实现循环

用loop指令实现循环

就是将”某些处理“封装到了Looptop内,使得代码更加简洁。


四、CISC和RISC


对比项目 CISC RISC
指令系统 复杂,庞大 简单,精简
指令数目 一般大于200条 一般小于100条
指令字长 不固定 定长
可访存指令 没有限制 只有Load/Store指令
各种指令执行时间 相差较大 绝大多数在一个周期内完成
各指令使用频率 有的常用,有点不常用 一般都常用
通用寄存器的数量 较少
控制方式 绝大多数为微程序控制 绝大多数为组合逻辑控制
指令流水线 可以通过一定方式实现 必须实现


相关文章
|
5月前
|
存储 算法 调度
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(下)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
|
5月前
|
存储 安全 网络安全
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(下)
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
|
5月前
|
存储 机器学习/深度学习 Unix
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
|
10月前
|
存储 编译器
考研计算机组成原理总结(8)
考研计算机组成原理总结(8)
125 0
|
10月前
|
存储 固态存储 程序员
考研计算机组成原理总结(5)
考研计算机组成原理总结(5)
298 0
|
10月前
|
存储 缓存 算法
考研计算机组成原理总结(7)
考研计算机组成原理总结(7)
184 0
|
10月前
|
存储 缓存 算法
考研计算机组成原理总结(6)
考研计算机组成原理总结(6)
480 0
|
10月前
|
存储 芯片 内存技术
考研计算机组成原理总结(4)
考研计算机组成原理总结(4)
207 0
考研计算机组成原理总结(4)
|
10月前
考研计算机组成原理总结(3)
考研计算机组成原理总结(3)
191 0

热门文章

最新文章