计算机组成原理--计算机指令

编译到汇编,代码从如何变成机器码

看下面的C代码示例:

#include <stdio.h>
int main()
{
   int a = 1; 
   int b = 2;

   a = a + b;
   
return 0 ;
}

通过编译和汇编后生成机器码

[root@localhost ~]# gcc -g -c test.c 
[root@localhost ~]# ls
anaconda-ks.cfg  test.c  test.o
[root@localhost ~]# objdump -d -M intel -S test.o
test.o:     文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
#include <stdio.h>
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
   
return 0 ;
  18:	b8 00 00 00 00       	mov    eax,0x0
}
  1d:	5d                   	pop    rbp
  1e:	c3                   	ret    

详细的转换关系如下图所示:

在这里插入图片描述
在inter 处理器中大概支持2000多种机器指令集,常见的指令可以分为下面的五类:
在这里插入图片描述

IF else 条件指令学习

#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;
  } 

对应上面if条件的指令如下:


    if (r == 0)
  3b:   83 7d fc 00             cmp    DWORD PTR [rbp-0x4],0x0
  3f:   75 09                   jne    4a <main+0x4a>
    {
        a = 1;
  41:   c7 45 f8 01 00 00 00    mov    DWORD PTR [rbp-0x8],0x1
  48:   eb 07                   jmp    51 <main+0x51>
    }
    else
    {
        a = 2;
  4a:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
  51:   b8 00 00 00 00          mov    eax,0x0
    } 

cmp 指令比较了前后两个操作数的值,这里的 DWORD PTR 代表操作的数据类型是 32 位的整数,而 [rbp-0x4] 则是一个寄存器的地址。所以,第一个操作数就是从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。cmp 指令的比较结果,会存入到条件码寄存器当中去。
跟着的 jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 4a 的位置。这个 4a,对应这里汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的 4a 这个地址。这个时候,CPU 再把 4a 地址里的指令加载到指令寄存器中来执行。跳转到执行地址为 4a 的指令,实际是一条 mov 指令,第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的寄存器地址,以及对应的 2 的 16 进制值 0x2。mov 指令把 2 设置到对应的寄存器里去,相当于一个赋值操作。然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。这条 mov 指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,它的作用是一个占位符。我们回过头去看前面的 if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令。跳转的地址就是这一行的地址 51。我们的 main 函数没有设定返回值,而 mov eax, 0x0 其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后也会跳转到这里,和 else 里的内容结束之后的位置是一样的。在这里插入图片描述

for 循环指令

int main()
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
	int a = 0;
   4:	c7 45 fc 00 00 00 00 	mov    DWORD PTR [rbp-0x4],0x0
	int i;
       for( i = 0; i < 5; 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 < 5; i ++)
  1a:	83 45 f8 01          	add    DWORD PTR [rbp-0x8],0x1
  1e:	83 7d f8 04          	cmp    DWORD PTR [rbp-0x8],0x4
  22:	7e f0                	jle    14 <main+0x14>
       }
return 0;
  24:	b8 00 00 00 00       	mov    eax,0x0
}
  29:	5d                   	pop    rbp
  2a:	c3                   	ret    

JLE ;有符号小于等于则跳转
JMP;无条件跳转
JNE;不等于侧跳转

switch 指令

int main()
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	48 83 ec 10          	sub    rsp,0x10
 char a = getchar();
   8:	e8 00 00 00 00       	call   d <main+0xd>
   d:	88 45 ff             	mov    BYTE PTR [rbp-0x1],al
int b = 0;
  10:	c7 45 f8 00 00 00 00 	mov    DWORD PTR [rbp-0x8],0x0
switch(a)
  17:	0f be 45 ff          	movsx  eax,BYTE PTR [rbp-0x1]
  1b:	83 f8 02             	cmp    eax,0x2
  1e:	74 13                	je     33 <main+0x33>
  20:	83 f8 03             	cmp    eax,0x3
  23:	74 17                	je     3c <main+0x3c>
  25:	83 f8 01             	cmp    eax,0x1
  28:	75 1a                	jne    44 <main+0x44>
{
case 1:
  b = 2;   
  2a:	c7 45 f8 02 00 00 00 	mov    DWORD PTR [rbp-0x8],0x2
break;
  31:	eb 11                	jmp    44 <main+0x44>
case 2:
  b = 9;   
  33:	c7 45 f8 09 00 00 00 	mov    DWORD PTR [rbp-0x8],0x9
break;
  3a:	eb 08                	jmp    44 <main+0x44>
case 3:
  b = 5;   
  3c:	c7 45 f8 05 00 00 00 	mov    DWORD PTR [rbp-0x8],0x5
break;
  43:	90                   	nop

}
return 0;
  44:	b8 00 00 00 00       	mov    eax,0x0

}
  49:	c9                   	leave  
  4a:	c3                   	ret 

if 条件多个的判断

    if (r== 0)
  4c:	83 7d fc 00          	cmp    DWORD PTR [rbp-0x4],0x0
  50:	75 09                	jne    5b <main+0x5b>
    {
	a = 1;
  52:	c7 45 f8 01 00 00 00 	mov    DWORD PTR [rbp-0x8],0x1
  59:	eb 16                	jmp    71 <main+0x71>
    }else if(r==1){
  5b:	83 7d fc 01          	cmp    DWORD PTR [rbp-0x4],0x1
  5f:	75 09                	jne    6a <main+0x6a>
        a = 2;
  61:	c7 45 f8 02 00 00 00 	mov    DWORD PTR [rbp-0x8],0x2
  68:	eb 07                	jmp    71 <main+0x71>
    }else{
       a = 3;
  6a:	c7 45 f8 03 00 00 00 	mov    DWORD PTR [rbp-0x8],0x3
}
}

从上面的汇编语言来看,判断条件大于2个话,使用switch性能更好,还有case分支中越容易发生的放在前面。

我们再看下面三目远算和if else 区别
当三目运算的数值非0和1时,和if esle 指令时一模一样的。具体看下面:

 b = (r == 0)?2:3;
  71:	83 7d fc 00          	cmp    DWORD PTR [rbp-0x4],0x0
  75:	75 07                	jne    7e <main+0x7e>
  77:	b8 02 00 00 00       	mov    eax,0x2
  7c:	eb 05                	jmp    83 <main+0x83>
  7e:	b8 03 00 00 00       	mov    eax,0x3
  83:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax
    b = (r == 0)?0:1;
  86:	83 7d fc 00          	cmp    DWORD PTR [rbp-0x4],0x0
  8a:	0f 95 c0             	setne  al
  8d:	0f b6 c0             	movzx  eax,al
  90:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax

函数调用

一个简单add函数操作和具体的汇编代码。

#include <stdio.h>

int static add(int a,int b)
{
    return a + b;
}

int main()
{
    int x = 5;
    int y = 10;
    int u = add(x,y);
    
    return 0;
}

汇编代码

int static add(int a,int b)
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
   7:	89 75 f8             	mov    DWORD PTR [rbp-0x8],esi
    return a + b;
   a:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
   d:	8b 55 fc             	mov    edx,DWORD PTR [rbp-0x4]
  10:	01 d0                	add    eax,edx
}
  12:	5d                   	pop    rbp
  13:	c3                   	ret    

0000000000000014 <main>:

int main()
{
  14:	55                   	push   rbp
  15:	48 89 e5             	mov    rbp,rsp
  18:	48 83 ec 10          	sub    rsp,0x10
    int x = 5;
  1c:	c7 45 fc 05 00 00 00 	mov    DWORD PTR [rbp-0x4],0x5
    int y = 10;
  23:	c7 45 f8 0a 00 00 00 	mov    DWORD PTR [rbp-0x8],0xa
    int u = add(x,y);
  2a:	8b 55 f8             	mov    edx,DWORD PTR [rbp-0x8]
  2d:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  30:	89 d6                	mov    esi,edx
  32:	89 c7                	mov    edi,eax
  34:	e8 c7 ff ff ff       	call   0 <add>
  39:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax
    
    return 0;
  3c:	b8 00 00 00 00       	mov    eax,0x0
}
  41:	c9                   	leave  
  42:	c3                   	ret  

对应上面函数 add 的汇编代码,我们来仔细看看,main 函数调用 add 函数时,add 函数入口在 0~1 行,add 函数结束之后在 12~13 行。我们在调用第 34 行的 call 指令时,会把当前的 PC 寄存器里的下一条指令的地址压栈,保留函数调用结束后要执行的指令地址。而 add 函数的第 0 行,push rbp 这个指令,就是在进行压栈。这里的 rbp 又叫栈帧指针(Frame Pointer),是一个存放了当前栈帧位置的寄存器。push rbp 就把之前调用函数,也就是 main 函数的栈帧的栈底地址,压到栈顶。接着,第 1 行的一条命令 mov rbp, rsp 里,则是把 rsp 这个栈指针(Stack Pointer)的值复制到 rbp 里,而 rsp 始终会指向栈顶。这个命令意味着,rbp 这个栈帧指针指向的地址,变成当前最新的栈顶,也就是 add 函数的栈帧的栈底地址了。而在函数 add 执行完成之后,又会分别调用第 12 行的 pop rbp 来将当前的栈顶出栈,这部分操作维护好了我们整个栈帧。然后,我们可以调用第 13 行的 ret 指令,这时候同时要把 call 调用的时候压入的 PC 寄存器里的下一条指令出栈,更新到 PC 寄存器中,将程序的控制权返回到出栈后的栈顶。

rbp 是属于当前函数的栈空间基地址,rsp 是包含当前函数为被调用函数准备的栈空间的基地址。

ELF 和静态链接

我们把上面的add示例进行拆分成add_lib.c和link.c,

//add_lib.c
int add(int a,int b)
{
    return a + b;
}
//link.c
#include <stdio.h>

int main()
{
    int a = 10;
    int b = 5;
    int c = add(a,b);

    printf("c = %d\n",c);

}

对应的各个汇编程序如下

[jinsh@localhost 08]$ objdump -d -M intel -S add_lib.o

add_lib.o:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <add>:
int add(int a,int b)
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
   7:	89 75 f8             	mov    DWORD PTR [rbp-0x8],esi
    return a + b;
   a:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
   d:	8b 55 fc             	mov    edx,DWORD PTR [rbp-0x4]
  10:	01 d0                	add    eax,edx
}
  12:	5d                   	pop    rbp
  13:	c3                   	ret    [jinsh@localhost 08]$ objdump -d -M intel -S link.o

link.o:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
#include <stdio.h>

int main()
{
   0:	55                   	push   rbp
   1:	48 89 e5             	mov    rbp,rsp
   4:	48 83 ec 10          	sub    rsp,0x10
    int a = 10;
   8:	c7 45 fc 0a 00 00 00 	mov    DWORD PTR [rbp-0x4],0xa
    int b = 5;
   f:	c7 45 f8 05 00 00 00 	mov    DWORD PTR [rbp-0x8],0x5
    int c = add(a,b);
  16:	8b 55 f8             	mov    edx,DWORD PTR [rbp-0x8]
  19:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  1c:	89 d6                	mov    esi,edx
  1e:	89 c7                	mov    edi,eax
  20:	b8 00 00 00 00       	mov    eax,0x0
  25:	e8 00 00 00 00       	call   2a <main+0x2a>
  2a:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax

    printf("c = %d\n",c);
  2d:	8b 45 f4             	mov    eax,DWORD PTR [rbp-0xc]
  30:	89 c6                	mov    esi,eax
  32:	bf 00 00 00 00       	mov    edi,0x0
  37:	b8 00 00 00 00       	mov    eax,0x0
  3c:	e8 00 00 00 00       	call   41 <main+0x41>

}
  41:	c9                   	leave  
  42:	c3                   	ret    

我们生成可执行可连接的elf文件

[jinsh@localhost 08]$ gcc -o link add_lib.o link.o
[jinsh@localhost 08]$ file link
link: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=095a881508fdcdedeac58205b272e626f9616c7f, not stripped```

第一个部分由编译(Compile)、汇编(Assemble)以及链接(Link)三个阶段组成。在这三个阶段完成之后,我们就生成了一个可执行文件。第二部分,我们通过装载器(Loader)把可执行文件装载(Load)到内存中。CPU 从内存中读取指令和数据,来开始真正执行程序。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191224213007707.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qaW5zaGFvaHVpLmJsb2cuY3Nkbi5uZXQ=,size_16,color_FFFFFF,t_70)
下面是ELF文件的基本组成部分
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191224213135823.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qaW5zaGFvaHVpLmJsb2cuY3Nkbi5uZXQ=,size_16,color_FFFFFF,t_70)

```c
[jinsh@localhost 08]$ objdump -d -M intel -S link

link:     文件格式 elf64-x86-64


Disassembly of section .init:

00000000004003e0 <_init>:
  4003e0:	48 83 ec 08          	sub    rsp,0x8
  4003e4:	48 8b 05 0d 0c 20 00 	mov    rax,QWORD PTR [rip+0x200c0d]        # 600ff8 <__gmon_start__>
  4003eb:	48 85 c0             	test   rax,rax
  4003ee:	74 05                	je     4003f5 <_init+0x15>
  4003f0:	e8 3b 00 00 00       	call   400430 <__gmon_start__@plt>
  4003f5:	48 83 c4 08          	add    rsp,0x8
  4003f9:	c3                   	ret    

Disassembly of section .plt:

0000000000400400 <.plt>:
  400400:	ff 35 02 0c 20 00    	push   QWORD PTR [rip+0x200c02]        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400406:	ff 25 04 0c 20 00    	jmp    QWORD PTR [rip+0x200c04]        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40040c:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]

0000000000400410 <printf@plt>:
  400410:	ff 25 02 0c 20 00    	jmp    QWORD PTR [rip+0x200c02]        # 601018 <printf@GLIBC_2.2.5>
  400416:	68 00 00 00 00       	push   0x0
  40041b:	e9 e0 ff ff ff       	jmp    400400 <.plt>

0000000000400420 <__libc_start_main@plt>:
  400420:	ff 25 fa 0b 20 00    	jmp    QWORD PTR [rip+0x200bfa]        # 601020 <__libc_start_main@GLIBC_2.2.5>
  400426:	68 01 00 00 00       	push   0x1
  40042b:	e9 d0 ff ff ff       	jmp    400400 <.plt>

0000000000400430 <__gmon_start__@plt>:
  400430:	ff 25 f2 0b 20 00    	jmp    QWORD PTR [rip+0x200bf2]        # 601028 <__gmon_start__>
  400436:	68 02 00 00 00       	push   0x2
  40043b:	e9 c0 ff ff ff       	jmp    400400 <.plt>

Disassembly of section .text:

0000000000400440 <_start>:
  400440:	31 ed                	xor    ebp,ebp
  400442:	49 89 d1             	mov    r9,rdx
  400445:	5e                   	pop    rsi
  400446:	48 89 e2             	mov    rdx,rsp
  400449:	48 83 e4 f0          	and    rsp,0xfffffffffffffff0
  40044d:	50                   	push   rax
  40044e:	54                   	push   rsp
  40044f:	49 c7 c0 00 06 40 00 	mov    r8,0x400600
  400456:	48 c7 c1 90 05 40 00 	mov    rcx,0x400590
  40045d:	48 c7 c7 41 05 40 00 	mov    rdi,0x400541
  400464:	e8 b7 ff ff ff       	call   400420 <__libc_start_main@plt>
  400469:	f4                   	hlt    
  40046a:	66 0f 1f 44 00 00    	nop    WORD PTR [rax+rax*1+0x0]

0000000000400470 <deregister_tm_clones>:
  400470:	b8 3f 10 60 00       	mov    eax,0x60103f
  400475:	55                   	push   rbp
  400476:	48 2d 38 10 60 00    	sub    rax,0x601038
  40047c:	48 83 f8 0e          	cmp    rax,0xe
  400480:	48 89 e5             	mov    rbp,rsp
  400483:	77 02                	ja     400487 <deregister_tm_clones+0x17>
  400485:	5d                   	pop    rbp
  400486:	c3                   	ret    
  400487:	b8 00 00 00 00       	mov    eax,0x0
  40048c:	48 85 c0             	test   rax,rax
  40048f:	74 f4                	je     400485 <deregister_tm_clones+0x15>
  400491:	5d                   	pop    rbp
  400492:	bf 38 10 60 00       	mov    edi,0x601038
  400497:	ff e0                	jmp    rax
  400499:	0f 1f 80 00 00 00 00 	nop    DWORD PTR [rax+0x0]

00000000004004a0 <register_tm_clones>:
  4004a0:	b8 38 10 60 00       	mov    eax,0x601038
  4004a5:	55                   	push   rbp
  4004a6:	48 2d 38 10 60 00    	sub    rax,0x601038
  4004ac:	48 c1 f8 03          	sar    rax,0x3
  4004b0:	48 89 e5             	mov    rbp,rsp
  4004b3:	48 89 c2             	mov    rdx,rax
  4004b6:	48 c1 ea 3f          	shr    rdx,0x3f
  4004ba:	48 01 d0             	add    rax,rdx
  4004bd:	48 d1 f8             	sar    rax,1
  4004c0:	75 02                	jne    4004c4 <register_tm_clones+0x24>
  4004c2:	5d                   	pop    rbp
  4004c3:	c3                   	ret    
  4004c4:	ba 00 00 00 00       	mov    edx,0x0
  4004c9:	48 85 d2             	test   rdx,rdx
  4004cc:	74 f4                	je     4004c2 <register_tm_clones+0x22>
  4004ce:	5d                   	pop    rbp
  4004cf:	48 89 c6             	mov    rsi,rax
  4004d2:	bf 38 10 60 00       	mov    edi,0x601038
  4004d7:	ff e2                	jmp    rdx
  4004d9:	0f 1f 80 00 00 00 00 	nop    DWORD PTR [rax+0x0]

00000000004004e0 <__do_global_dtors_aux>:
  4004e0:	80 3d 4d 0b 20 00 00 	cmp    BYTE PTR [rip+0x200b4d],0x0        # 601034 <_edata>
  4004e7:	75 11                	jne    4004fa <__do_global_dtors_aux+0x1a>
  4004e9:	55                   	push   rbp
  4004ea:	48 89 e5             	mov    rbp,rsp
  4004ed:	e8 7e ff ff ff       	call   400470 <deregister_tm_clones>
  4004f2:	5d                   	pop    rbp
  4004f3:	c6 05 3a 0b 20 00 01 	mov    BYTE PTR [rip+0x200b3a],0x1        # 601034 <_edata>
  4004fa:	f3 c3                	repz ret 
  4004fc:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]

0000000000400500 <frame_dummy>:
  400500:	48 83 3d 18 09 20 00 	cmp    QWORD PTR [rip+0x200918],0x0        # 600e20 <__JCR_END__>
  400507:	00 
  400508:	74 1e                	je     400528 <frame_dummy+0x28>
  40050a:	b8 00 00 00 00       	mov    eax,0x0
  40050f:	48 85 c0             	test   rax,rax
  400512:	74 14                	je     400528 <frame_dummy+0x28>
  400514:	55                   	push   rbp
  400515:	bf 20 0e 60 00       	mov    edi,0x600e20
  40051a:	48 89 e5             	mov    rbp,rsp
  40051d:	ff d0                	call   rax
  40051f:	5d                   	pop    rbp
  400520:	e9 7b ff ff ff       	jmp    4004a0 <register_tm_clones>
  400525:	0f 1f 00             	nop    DWORD PTR [rax]
  400528:	e9 73 ff ff ff       	jmp    4004a0 <register_tm_clones>

000000000040052d <add>:
int add(int a,int b)
{
  40052d:	55                   	push   rbp
  40052e:	48 89 e5             	mov    rbp,rsp
  400531:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
  400534:	89 75 f8             	mov    DWORD PTR [rbp-0x8],esi
    return a + b;
  400537:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
  40053a:	8b 55 fc             	mov    edx,DWORD PTR [rbp-0x4]
  40053d:	01 d0                	add    eax,edx
}
  40053f:	5d                   	pop    rbp
  400540:	c3                   	ret    

0000000000400541 <main>:
#include <stdio.h>

int main()
{
  400541:	55                   	push   rbp
  400542:	48 89 e5             	mov    rbp,rsp
  400545:	48 83 ec 10          	sub    rsp,0x10
    int a = 10;
  400549:	c7 45 fc 0a 00 00 00 	mov    DWORD PTR [rbp-0x4],0xa
    int b = 5;
  400550:	c7 45 f8 05 00 00 00 	mov    DWORD PTR [rbp-0x8],0x5
    int c = add(a,b);
  400557:	8b 55 f8             	mov    edx,DWORD PTR [rbp-0x8]
  40055a:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  40055d:	89 d6                	mov    esi,edx
  40055f:	89 c7                	mov    edi,eax
  400561:	b8 00 00 00 00       	mov    eax,0x0
  400566:	e8 c2 ff ff ff       	call   40052d <add>
  40056b:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax

    printf("c = %d\n",c);
  40056e:	8b 45 f4             	mov    eax,DWORD PTR [rbp-0xc]
  400571:	89 c6                	mov    esi,eax
  400573:	bf 20 06 40 00       	mov    edi,0x400620
  400578:	b8 00 00 00 00       	mov    eax,0x0
  40057d:	e8 8e fe ff ff       	call   400410 <printf@plt>

}
  400582:	c9                   	leave  
  400583:	c3                   	ret    
  400584:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
  40058b:	00 00 00 
  40058e:	66 90                	xchg   ax,ax

0000000000400590 <__libc_csu_init>:
  400590:	41 57                	push   r15
  400592:	41 89 ff             	mov    r15d,edi
  400595:	41 56                	push   r14
  400597:	49 89 f6             	mov    r14,rsi
  40059a:	41 55                	push   r13
  40059c:	49 89 d5             	mov    r13,rdx
  40059f:	41 54                	push   r12
  4005a1:	4c 8d 25 68 08 20 00 	lea    r12,[rip+0x200868]        # 600e10 <__frame_dummy_init_array_entry>
  4005a8:	55                   	push   rbp
  4005a9:	48 8d 2d 68 08 20 00 	lea    rbp,[rip+0x200868]        # 600e18 <__init_array_end>
  4005b0:	53                   	push   rbx
  4005b1:	4c 29 e5             	sub    rbp,r12
  4005b4:	31 db                	xor    ebx,ebx
  4005b6:	48 c1 fd 03          	sar    rbp,0x3
  4005ba:	48 83 ec 08          	sub    rsp,0x8
  4005be:	e8 1d fe ff ff       	call   4003e0 <_init>
  4005c3:	48 85 ed             	test   rbp,rbp
  4005c6:	74 1e                	je     4005e6 <__libc_csu_init+0x56>
  4005c8:	0f 1f 84 00 00 00 00 	nop    DWORD PTR [rax+rax*1+0x0]
  4005cf:	00 
  4005d0:	4c 89 ea             	mov    rdx,r13
  4005d3:	4c 89 f6             	mov    rsi,r14
  4005d6:	44 89 ff             	mov    edi,r15d
  4005d9:	41 ff 14 dc          	call   QWORD PTR [r12+rbx*8]
  4005dd:	48 83 c3 01          	add    rbx,0x1
  4005e1:	48 39 eb             	cmp    rbx,rbp
  4005e4:	75 ea                	jne    4005d0 <__libc_csu_init+0x40>
  4005e6:	48 83 c4 08          	add    rsp,0x8
  4005ea:	5b                   	pop    rbx
  4005eb:	5d                   	pop    rbp
  4005ec:	41 5c                	pop    r12
  4005ee:	41 5d                	pop    r13
  4005f0:	41 5e                	pop    r14
  4005f2:	41 5f                	pop    r15
  4005f4:	c3                   	ret    
  4005f5:	90                   	nop
  4005f6:	66 2e 0f 1f 84 00 00 	nop    WORD PTR cs:[rax+rax*1+0x0]
  4005fd:	00 00 00 

0000000000400600 <__libc_csu_fini>:
  400600:	f3 c3                	repz ret 

Disassembly of section .fini:

0000000000400604 <_fini>:
  400604:	48 83 ec 08          	sub    rsp,0x8
  400608:	48 83 c4 08          	add    rsp,0x8
  40060c:	c3                   	ret   

ELF 文件格式把各种信息,分成一个一个的 Section 保存起来。ELF 有一个基本的文件头(File Header),用来表示这个文件的基本属性,比如是否是可执行文件,对应的 CPU、操作系统等等。除了这些基本属性之外,大部分程序还有这么一些 Section:
首先是.text Section,也叫作代码段或者指令段(Code Section),用来保存程序的代码和指令;接着是.data Section,也叫作数据段(Data Section),用来保存程序里面设置好的初始化数据信息;
然后就是.rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是当前的文件里面,哪些跳转地址其实是我们不知道的。比如上面的 link_example.o 里面,我们在 main 函数里面调用了 add 和 printf 这两个函数,但是在链接发生之前,我们并不知道该跳转到哪里,这些信息就会存储在重定位表里;

[jinsh@localhost 08]$ objdump -r link.o

link.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000026 R_X86_64_PC32     add-0x0000000000000004
0000000000000033 R_X86_64_32       .rodata
000000000000003d R_X86_64_PC32     printf-0x0000000000000004


RELOCATION RECORDS FOR [.debug_info]:
OFFSET           TYPE              VALUE 
0000000000000006 R_X86_64_32       .debug_abbrev
000000000000000c R_X86_64_32       .debug_str+0x0000000000000027
0000000000000011 R_X86_64_32       .debug_str+0x0000000000000020
0000000000000015 R_X86_64_32       .debug_str+0x000000000000006f
0000000000000019 R_X86_64_64       .text
0000000000000029 R_X86_64_32       .debug_line
0000000000000030 R_X86_64_32       .debug_str+0x0000000000000097
0000000000000037 R_X86_64_32       .debug_str+0x00000000000000ae
000000000000003e R_X86_64_32       .debug_str
0000000000000045 R_X86_64_32       .debug_str+0x0000000000000013
000000000000004c R_X86_64_32       .debug_str+0x00000000000000ca
0000000000000053 R_X86_64_32       .debug_str+0x00000000000000d6
0000000000000061 R_X86_64_32       .debug_str+0x00000000000000c1
0000000000000068 R_X86_64_32       .debug_str+0x00000000000000e0
000000000000006f R_X86_64_32       .debug_str+0x00000000000000a9
0000000000000074 R_X86_64_32       .debug_str+0x00000000000000bc
000000000000007e R_X86_64_64       .text


RELOCATION RECORDS FOR [.debug_aranges]:
OFFSET           TYPE              VALUE 
0000000000000006 R_X86_64_32       .debug_info
0000000000000010 R_X86_64_64       .text


RELOCATION RECORDS FOR [.debug_line]:
OFFSET           TYPE              VALUE 
000000000000002a R_X86_64_64       .text


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

最后是.symtab Section,叫作符号表(Symbol Table)。符号表保留了我们所说的当前文件里面定义的函数名称和对应地址的地址簿。我们可以通过下面的命令来查看符号表:

[jinsh@localhost 08]$ readelf -s link

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

Symbol table '.symtab' contains 70 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000400238     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000400254     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000400274     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000400298     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000004002b8     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000400318     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000400358     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000400360     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000400380     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000400398     0 SECTION LOCAL  DEFAULT   10 
    11: 00000000004003e0     0 SECTION LOCAL  DEFAULT   11 
    12: 0000000000400400     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000400440     0 SECTION LOCAL  DEFAULT   13 
    14: 0000000000400604     0 SECTION LOCAL  DEFAULT   14 
    15: 0000000000400610     0 SECTION LOCAL  DEFAULT   15 
    16: 0000000000400628     0 SECTION LOCAL  DEFAULT   16 
    17: 0000000000400668     0 SECTION LOCAL  DEFAULT   17 
    18: 0000000000600e10     0 SECTION LOCAL  DEFAULT   18 
    19: 0000000000600e18     0 SECTION LOCAL  DEFAULT   19 
    20: 0000000000600e20     0 SECTION LOCAL  DEFAULT   20 
    21: 0000000000600e28     0 SECTION LOCAL  DEFAULT   21 
    22: 0000000000600ff8     0 SECTION LOCAL  DEFAULT   22 
    23: 0000000000601000     0 SECTION LOCAL  DEFAULT   23 
    24: 0000000000601030     0 SECTION LOCAL  DEFAULT   24 
    25: 0000000000601034     0 SECTION LOCAL  DEFAULT   25 
    26: 0000000000000000     0 SECTION LOCAL  DEFAULT   26 
    27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
    28: 0000000000000000     0 SECTION LOCAL  DEFAULT   28 
    29: 0000000000000000     0 SECTION LOCAL  DEFAULT   29 
    30: 0000000000000000     0 SECTION LOCAL  DEFAULT   30 
    31: 0000000000000000     0 SECTION LOCAL  DEFAULT   31 
    32: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    33: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   20 __JCR_LIST__
    34: 0000000000400470     0 FUNC    LOCAL  DEFAULT   13 deregister_tm_clones
    35: 00000000004004a0     0 FUNC    LOCAL  DEFAULT   13 register_tm_clones
    36: 00000000004004e0     0 FUNC    LOCAL  DEFAULT   13 __do_global_dtors_aux
    37: 0000000000601034     1 OBJECT  LOCAL  DEFAULT   25 completed.6355
    38: 0000000000600e18     0 OBJECT  LOCAL  DEFAULT   19 __do_global_dtors_aux_fin
    39: 0000000000400500     0 FUNC    LOCAL  DEFAULT   13 frame_dummy
    40: 0000000000600e10     0 OBJECT  LOCAL  DEFAULT   18 __frame_dummy_init_array_
    41: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS add_lib.c
    42: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS link.c
    43: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    44: 0000000000400778     0 OBJECT  LOCAL  DEFAULT   17 __FRAME_END__
    45: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   20 __JCR_END__
    46: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    47: 0000000000600e18     0 NOTYPE  LOCAL  DEFAULT   18 __init_array_end
    48: 0000000000600e28     0 OBJECT  LOCAL  DEFAULT   21 _DYNAMIC
    49: 0000000000600e10     0 NOTYPE  LOCAL  DEFAULT   18 __init_array_start
    50: 0000000000400628     0 NOTYPE  LOCAL  DEFAULT   16 __GNU_EH_FRAME_HDR
    51: 0000000000601000     0 OBJECT  LOCAL  DEFAULT   23 _GLOBAL_OFFSET_TABLE_
    52: 0000000000400600     2 FUNC    GLOBAL DEFAULT   13 __libc_csu_fini
    53: 0000000000601030     0 NOTYPE  WEAK   DEFAULT   24 data_start
    54: 000000000040052d    20 FUNC    GLOBAL DEFAULT   13 add
    55: 0000000000601034     0 NOTYPE  GLOBAL DEFAULT   24 _edata
    56: 0000000000400604     0 FUNC    GLOBAL DEFAULT   14 _fini
    57: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    58: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    59: 0000000000601030     0 NOTYPE  GLOBAL DEFAULT   24 __data_start
    60: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    61: 0000000000400618     0 OBJECT  GLOBAL HIDDEN    15 __dso_handle
    62: 0000000000400610     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used
    63: 0000000000400590   101 FUNC    GLOBAL DEFAULT   13 __libc_csu_init
    64: 0000000000601038     0 NOTYPE  GLOBAL DEFAULT   25 _end
    65: 0000000000400440     0 FUNC    GLOBAL DEFAULT   13 _start
    66: 0000000000601034     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start
    67: 0000000000400541    67 FUNC    GLOBAL DEFAULT   13 main
    68: 0000000000601038     0 OBJECT  GLOBAL HIDDEN    24 __TMC_END__
    69: 00000000004003e0     0 FUNC    GLOBAL DEFAULT   11 _init

程序装载到内存

说起来只是装载到内存里面这一句话的事儿,实际上装载器需要满足两个要求。
第一,可执行程序加载后占用的内存空间应该是连续的。执行指令的时候,程序计数器是顺序地一条一条指令执行下去。这也就意味着,这一条条指令需要连续地存储在一起。
第二,我们需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置。虽然编译出来的指令里已经有了对应的各种各样的内存地址,但是实际加载的时候,我们其实没有办法确保,这个程序一定加载在哪一段内存地址上。因为我们现在的计算机通常会同时运行很多个程序,可能你想要的内存地址已经被其他加载了的程序占用了。
我们把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫物理内存地址(Physical Memory Address)。程序里有指令和各种内存地址,我们只需要关心虚拟内存地址就行了。对于任何一个程序来说,它看到的都是同样的内存地址。我们维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。因为是连续的内存地址空间,所以我们只需要维护映射关系的起始地址和对应的空间大小就可以了。

我们来看这样一个例子。我现在手头的这台电脑,有 1GB 的内存。我们先启动一个图形渲染程序,占用了 512MB 的内存,接着启动一个 Chrome 浏览器,占用了 128MB 内存,再启动一个 Python 程序,占用了 256MB 内存。这个时候,我们关掉 Chrome,于是空闲内存还有 1024 - 512 - 256 = 256MB。按理来说,我们有足够的空间再去装载一个 200MB 的程序。但是,这 256MB 的内存空间不是连续的,而是被分成了两段 128MB 的内存。因此,实际情况是,我们的程序没办法加载进来。在这里插入图片描述
当然,这个我们也有办法解决。解决的办法叫内存交换(Memory Swapping)。我们可以把 Python 程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里面。不过读回来的时候,我们不再把它加载到原来的位置,而是紧紧跟在那已经被占用了的 512MB 内存后面。这样,我们就有了连续的 256MB 内存空间,就可以去加载一个新的 200MB 的程序。如果你自己安装过 Linux 操作系统,你应该遇到过分配一个 swap 硬盘分区的问题。这块分出来的磁盘空间,其实就是专门给 Linux 操作系统进行内存交换用的。虚拟内存、分段,再加上内存交换,看起来似乎已经解决了计算机同时装载运行很多个程序的问题。不过,你千万不要大意,这三者的组合仍然会遇到一个性能瓶颈。硬盘的访问速度要比内存慢很多,而每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是一个很占内存空间的程序,这样整个机器都会显得卡顿。

内存分页
既然问题出在内存碎片和内存交换的空间太大上,那么解决问题的办法就是,少出现一些内存碎片。另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决这个问题。这个办法,在现在计算机的内存管理里面,就叫作内存分页(Paging)。
和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小。而对应的程序所需要占用的虚拟内存空间,也会同样切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。从虚拟内存到物理内存的映射,不再是拿整段连续的内存的物理地址,而是按照一个一个页来的。页的尺寸一般远远小于整个程序的大小。在 Linux 下,我们通常只设置成 4KB。你可以通过命令看看你手头的 Linux 系统设置的页的大小。

在这里插入图片描述```c
[jinsh@localhost 08]$ getconf PAGE_SIZE
4096


更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。实际上,我们的操作系统,的确是这么做的。当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。同时,这样一来,任何程序都不需要一次性加载完所有指令和数据,只需要加载当前需要用到就行了。通过虚拟内存、内存交换和内存分页这三个技术的组合,我们最终得到了一个让程序不需要考虑实际的物理内存地址、大小和当前分配空间的解决方案。这些技术和方法,对于我们程序的编写、编译和链接过程都是透明的。这也是我们在计算机的软硬件开发中常用的一种方法,就是加入一个间接层。通过引入虚拟内存、页映射和内存交换,我们的程序本身,就不再需要考虑对应的真实的内存地址、程序加载、内存管理等问题了。任何一个程序,都只需要把内存当成是一块完整而连续的空间来直接使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值