计算机指令-程序执行

之前理解了机器码的生成和执行过程,从高级语言到机器码,然后理解了if else如何通过jmp jne这种指令实现的,同时理解了栈的执行逻辑。
但是如果底层CPU执行的机器码是一样的,为何windows的程序和linux的程序不能通用呢。

ELF和静态链接

如下是经过汇编器编译后的add.o函数和link.o两个文件,

add_lib.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:
   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
   a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
   d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  10:   01 d0                   add    eax,edx
  12:   5d                      pop    rbp
  13:   c3                      ret    

link_example.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
   f:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
  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> //这里call指定的地址是下一行2a地址,并不是add方法地址。
  2a:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  2d:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
  30:   89 c6                   mov    esi,eax
  32:   48 8d 3d 00 00 00 00    lea    rdi,[rip+0x0]        # 39 <main+0x39>
  39:   b8 00 00 00 00          mov    eax,0x0
  3e:   e8 00 00 00 00          call   43 <main+0x43>
  43:   b8 00 00 00 00          mov    eax,0x0
  48:   c9                      leave  
  49:   c3                      ret    

我们发现几个问题:

  • 两个文件地址是重复的。
  • link.o文件里call方法指定的地址也不是add.o的起始地址。
  • 这两个文件无法执行,执行会报错。

目标文件/链接器/可执行文件/ELF格式

无论是这里的运行报错,还是 objdump 出来的汇编代码里面的重复地址,都是因为 add_lib.o 以及 link_example.o 并不是一个可执行文件(Executable Program),而是目标文件(Object File)。只有通过链接器(Linker)把多个目标文件以及调用的各种函数库链接起来,我们才能得到一个可执行文件。同时目标文件和可执行文件都符合ELF格式,这是linux系统下的文件格式。

高级程序->CPU执行

如下是高级程序到CPU执行的真正流程,之前的编译、汇编是简单流程。

如下是经过链接器链接后的可执行文件:

link_example:     file format elf64-x86-64
Disassembly of section .init:
...
Disassembly of section .plt:
...
Disassembly of section .plt.got:
...
Disassembly of section .text:
...

 6b0:   55                      push   rbp
 6b1:   48 89 e5                mov    rbp,rsp
 6b4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
 6b7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
 6ba:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 6bd:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 6c0:   01 d0                   add    eax,edx
 6c2:   5d                      pop    rbp
 6c3:   c3                      ret    
00000000000006c4 <main>:
 6c4:   55                      push   rbp
 6c5:   48 89 e5                mov    rbp,rsp
 6c8:   48 83 ec 10             sub    rsp,0x10
 6cc:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
 6d3:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
 6da:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
 6dd:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
 6e0:   89 d6                   mov    esi,edx
 6e2:   89 c7                   mov    edi,eax
 6e4:   b8 00 00 00 00          mov    eax,0x0
 6e9:   e8 c2 ff ff ff          call   6b0 <add>
 6ee:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
 6f1:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
 6f4:   89 c6                   mov    esi,eax
 6f6:   48 8d 3d 97 00 00 00    lea    rdi,[rip+0x97]        # 794 <_IO_stdin_used+0x4>
 6fd:   b8 00 00 00 00          mov    eax,0x0
 702:   e8 59 fe ff ff          call   560 <printf@plt>
 707:   b8 00 00 00 00          mov    eax,0x0
 70c:   c9                      leave  
 70d:   c3                      ret    
 70e:   66 90                   xchg   ax,ax
...
Disassembly of section .fini:
...

可以看到上面的可执行文件,地址已经不重复了,call后面调用的也是add函数的第一行指令,文件本身也可以直接执行了。

ELF格式

在 Linux 下,可执行文件和目标文件所使用的都是一种叫 ELF(Execuatable and Linkable File Format)的文件格式,中文名字叫可执行与可链接文件格式,这里面不仅存放了编译成的汇编指令,还保留了很多别的数据。
如下是ELF文件的格式:

如下是目标文件通过链接器链接为可执行文件的流程:

总结

回到开头,因为windows下是PE文件格式和linux的ELF文件格式不匹配,所以两个无法执行。

程序装载和执行

之前讲到可执行文件装载到内存中之后,就可以由CPU执行,但这里省略了一大个步骤:一个计算机有N个程序,都想要加载到内存中,操作系统是如何管理内存来加载这些程序呢?

虚拟内存技术

因为内存也是N个寄存器的矩阵,取址和读取也是耗费时间的,所以对于程序来说,最好程序在一个连续的内存地址中,这样PC寄存器就可以连续读取指令,速度就会很快。
而同时,计算机会有多个程序想要加载到内存,各自私自申请就会造成错误和浪费,此时操作系统就站出来了,发明了【虚拟内存技术】来管理所有程序加载到内存的过程。

如下图所示:对程序A和程序B来说,其虚拟内存地址都是从0x400000开始的,但物理内存地址是不同的。这个虚拟内存=>物理内存的映射就由操作系统维护管理,每个程序只需要关心起虚拟内存地址就可以了。

我们把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫物理内存地址(Physical Memory Address)。

内存交换技术

有虚拟内存技术后,内存管理已经不成问题,但是在时间维度上看,程序会装载,卸载,再装载,这个过程就会导致很多物理内存被切碎,如下图,这种就是【内存碎片问题(Memory Fragmentation)】,和磁盘碎片问题一样,这种内存碎片会导致明明有足够空间,但是无法加载程序。

解决方案也很简单:【内存交换技术(Memory Swapping)】,操作系统可以把需要整理的内存空间程序,先加载到磁盘上(也就是安装Linux系统时,需要分配的swap硬盘分区),然后整理内存,最后把程序从磁盘上加载回内存上。

内存分页技术

以上一些技术手段,看起来已经能够解决问题了,但是不然,实际情况是:当内存交换发生时,系统会显得很卡,因为对整个内存的整理是一个非常耗时的操作。
于是就像jdk11的ZGC技术一样,可以把内存化整为零,切割成一个个标准的小块(实际上应该是Java学习操作系统才发明的ZGC),例如4K大小,也就是【内存页 Page】来装载程序。

$ getconf PAGE_SIZE //我们可以用这个命令看看操作系统上的内存页大小。

有了内存页,我们就只需要在内存不足时交换几个内存页来整理空间,同时我们也可以对程序加载分步骤,暂时没用到的程序可以不加载到内存中,如果读取特定的页不存在时,触发一个来自于 CPU 的缺页错误(Page Fault),然后再加载该页程序。

通过引入虚拟内存、页映射和内存交换,我们的程序本身,就不再需要考虑对应的真实的内存地址、程序加载、内存管理等问题了。任何一个程序,都只需要把内存当成是一块完整而连续的空间来直接使用。

动态链接

之前我们讲的目标文件通过Linker变成可执行文件,这个过程叫做【静态链接(Static Link)】,他在程序执行前就通过合并文件完成了函数逻辑衔接。这种方式很明显,会导致文件特别大,假如每个程序都需要用到一个加法函数,按静态链接方式,每个函数内都要塞一段加法函数指令,文件就会膨胀,内存肯定塞不下。

这时候就很自然想到了【动态链接(Dynamic Link)】,设置一个公共函数库(Shared Libraries),在执行时,进行跳转调用函数指令。

在 Windows 下,这些共享库文件就是.dll 文件,也就是 Dynamic-Link Libary(DLL,动态链接库)。在 Linux 下,这些共享库文件就是.so 文件,也就是 Shared Object(一般我们也称之为动态链接库)

posted @ 2023-03-15 10:01  来焕明  阅读(87)  评论(0)    收藏  举报