当前位置: 首页 > news >正文

淮安软件园有做网站的吗经营性商务网站建设需要备案吗

淮安软件园有做网站的吗,经营性商务网站建设需要备案吗,引擎优化,网页设计是什么职业引言 随着越来越多功能强大的高级语言的出现#xff0c;在服务器计算能力不是瓶颈的条件下#xff0c;很多同学会选择开发效率高#xff0c;功能强大的虚拟机支持的高级语言#xff08;Java#xff09;#xff0c;或者脚本语言#xff08;Python#xff0c;Php#xf… 引言 随着越来越多功能强大的高级语言的出现在服务器计算能力不是瓶颈的条件下很多同学会选择开发效率高功能强大的虚拟机支持的高级语言Java或者脚本语言PythonPhp作为实现功能的首选而不会选择开发效率低而运行效率高的 C/C 作为开发语言。而这些语言一般情况下是运行在虚拟机或者解释器中而不需要直接跟操作系统直接打交道。 虚拟机和解释器相当于为高级语言或者脚本语言提供了一个中间层隔离了与操作系统之间进行交互的细节这为工程师们减少了很多与系统底层打交道的麻烦大大提高了工程师的开发效率。但是这样也造成了工程师们长期工作在高级语言之上在有时候需要与链接库可执行文件CPU 体系结构这些概念有交互的时候会比较陌生。 因此本文为了让读者可以对源代码如何编译到二进制可执行程序有一个整体的了解将会从一下几个方面介绍一下程序编译链接和装载的基本原理。 首先我们介绍一下不同的 CPU 体系结构和不同的操作系统对应的可执行文件的格式。然后以几个简单的 C 程序为例子介绍一下编译器和链接器对程序源代码所做的处理。最后我们看一下程序执行的时候装载器对程序的处理以及操作系统对其的支持。CPU体系结构 我们现在大部分同学接触到的 PC 机或者服务器使用的 CPU 都是 X86_64 指令集体系结构这是一种基于 CISC复杂指令集体系结构。我们在计算机组成原理的课程里面都学到其实CPU指令集类型中除了 CISC还有另外一种 RISC 类型的 CPU 体系结构也就是简单指令集体系结构比如 SUN 的 SPARC 指令集IBM 的 PowerPC 指令集都是基于 RISC 指令集的 CPU 体系结构。我们这里不去深究各种体系结构的细节我们关心的是在其中一种 CPU 体系结构中编译的代码能够在另一种体系结构下面运行么 答案是否定的因为所谓的二进制程序其实都是有一条一条的 CPU 指令组成二进制程序执行的过程中也是由 CPU 把这些指令 load 到指令流中一条一条执行。不同的 CPU 体系结构的指令集是不一样的指令的长度和组成都有区别。所以让SPARC的CPU去执行一个编译成 X86 的 CPU 指令集的二进制程序是不可行的。 操作系统 记得当年 Java 语言刚兴起的时候有一个很大的卖点就是跨平台执行。为什么能够跨平台执行呢这是因为 Java 程序经过 Javac 编译之后得到了一种 Java 虚拟机可以执行的字节码文件。只要在不同的操作系统上WindowsLinuxMacOS上装上自己所属版本的 Java 虚拟机之后就可以执行在另外一种操作系统下面编译的 Java 字节码程序。那么为什么经过 gcc/g 编译过的二进制程序不能跨平台执行呢 我们刚才说了Java 程序能够跨平台执行是因为不同系统平台上面安装的 Java 虚拟机能够识别同一种 Java 字节码。那么我们是不是可以推断不同的操作系统二进制程序不能跨平台执行是因为不同操作系统下面二进制文件的格式不同呢 事实确实是这样的。我们都知道的是一个程序编译成二进制之后运行的时候是从 main 函数开始执行的。但是这个程序是怎么样 load 到内存中的执行流又是如何准确的定位到 main 函数的地址的。其实这些工作都是操作系统替我们做的。我们可以大胆想一下main 函数之前操作系统都需要做哪些工作。首先操作系统肯定要分配一块虚拟地址空间然后系统需要把二进制程序中的代码和数据 load 到这个地址空间中随后系统会根据某种特定的文件格式找到其中某一个特定的位置初始化段做一些程序运行前的初始化工作比如环境变量初始化和全局变量的处理然后开始执行我们的 main 函数。这里的“某种特定的文件格式”就是为什么二进制程序不能跨平台运行的原因。 这里我真正想说的是每一种操作系统有自己的二进制文件格式操作系统把二进制可执行程序load到内存中之后会根据默认的这种格式寻找各种数据比如代码段数据段和初始化段。所以说 Windows 下面的 exe 可执行文件lib 静态库dll 动态库是不可以直接运行在 Linux 系统下面的MacOS 下面的 Mach-O 可执行文件静态链接库a库动态链接库so库也是不能够直接放在 Linux 系统下面运行的。反之亦然Linux 下面的 ELF 可执行文件静态链接库a库动态链接库so库同样不能够在 Window 系统下面运行。 源代码的编译 说完了 CPU 体系结构和操作系统对二进制文件格式的影响下面我们从几个例子看一下从源代码文件如何经过处理最终变成一个可执行文件。不同的系统下有不同的编译器比如 Windows 下有 vs 自带的 C 编译器Linux 和 Unix 下面有 gcc/g 编译器。也有很多不同的编程语言各自有自己的编译器把相应的源代码编译成二进制可执行程序。尽管有些实现细节不同这些编译器的工作原理和过程是一致的。由于 Linux 和 c语言使用相对广泛同时笔者对 Linux 和C/C相对熟悉本文剩下的部分都是基于在 Linux 平台下使用 gcc/g 编译器编译 c/c 源代码进行说明和解释。 本文的初衷是让工程师对程序源代码如何通过编译器链接器和装载器最终成为一个进程运行在系统中的整个过程有一个基本的理解所以并不会涉及到编译器如何通过进行词法分析语法分析和语义分析最终得到目标二进制文件。因此本文剩下的部分主要集中在 gcc/g 如何形成一个 Linux 认识的 elf 可执行文件的。 C源码文件 首先我们简单回顾下 C 源码程序中变量和函数的基本概念。 我们先来区分一下声明和定义的概念。在 C 语言程序中我们可以声明一个变量和或者一个函数也可以定义一个变量或者函数。这两个的区别如下 声明一个全局变量或者函数是告诉编译器在当前的源文件中可能会用到这个变量或者调用这个函数但是这个变量或者函数不在当前文件中定义而会在其他的某个文件中定义请编译器编译本文件的时候不要报错。定义一个变量是告诉编译器在生成的目标文件中预留一个空间如果变量有初始值请编译器在目标文件中保存这个初始值。定义一个函数是请编译器在这个文件的目标文件中生成这个函数的二进制代码。然后我们需要来看一下 C 源码程序中的变量类型和函数类型最基本的 C 程序不使用 C 功能是比较简单的我们可以声明定义变量和局部变量全局变量和局部变量可以声明为 static 变量和非 static 变量。除此之外我们可以通过 malloc 动态申请变量。他们的区别如下 非 static 全局变量表示这个变量存在于程序执行的整个生命周期同时可以被本源码文件之外的其他文件访问到。static 全局变量表示这个变量存在于程序执行的整个生命周期但是只能被本源码文件的函数访问到。非 static 局部变量表示这个变量只在本变量所在的函数的执行上下文中存在实际上这种变量是在函数执行栈的函数栈帧中static 局部变量其实属于全部变量的范畴它存在于程序执行的整个生命周期但是作用域被局限在定义这个变量的代码块中大括号包含的范围动态申请的变量表明这个变量是在运行过程中由函数动态的从进程的地址空间中申请一块空间并使用这个空间存储数据。对于函数而言我们同样可以定义 static 和非 static 的函数区别如下 非 static 函数定义表明这是一个全局的函数可以被本源码文件的其他文件访问到。static 函数限制了本函数只能被本源码文件的函数调用。我们用下面这个小程序来看看编译器对上述这些变量都做了什么样的处理。 int g_a 1; //定义有初始值全局变量 int g_b; //定义无初始值全局变量 static int g_c; //定义全局static变量 extern int g_x; //声明全局变量 extern int sub(); //函数声明int sum(int m, int n) { //函数定义return mn; } int main(int argc, char* argv[]) {static int s_a 0; //局部static变量int l_a 0; //局部非static变量sum(g_a,g_b);return 0; }目标文件 在我们用 gcc 编译这个程序来查看编译器做了哪些事情之前其实我们可以简单梳理一下为了让这个程序能够运行起来编译器至少都需要做哪些事情。首先我们都会默认 CPU 会根据程序的代码一条一条执行其实当遇到了条件判断或者函数调用程序就会发生指令流的跳转还有程序代码执行的过程中需要操作各种变量指向的数据。从这三个“理所当然”的行为里面我们可以推断出编译器至少需要做哪些事情。第一CPU 肯定不能理解这些高级语言代码编译器需要把代码编译成二进制指令。 第二指令流跳转的时候CPU 怎么能找到要跳转的位置编译器需要为每个定义的函数所在的位置定义一个标签每个标签有一个地址调用每个函数的时候就相当于跳转到那个标签指向的地址。 第三CPU 如何能找到那些变量指向的数据编译器需要为每一个变量定义一个标签每个标签同样有一个地址这个地址指向内存中的数据空间。 我们来实际看一下编译器的行为我们先把这个这个编译成目标文件看一下 gcc -c test.c -o test.o nm test.o 0000000000000000 D g_a 0000000000000004 C g_b 0000000000000000 b g_cU g_x 0000000000000014 T main 0000000000000004 b s_a.1597 0000000000000000 T sum首先我们用 gcc -c 命令把 test.c 源码文件编译成 test.o 目标文件需要注意的是虽然目标文件也是二进制文件但是和可执行文件是有区别的目标文件仅仅把当前的源码文件编译成二进制文件并没有经过链接过程是不能够执行的。然后我们用 nm 命令可以查看一下目标文件的 symbol 信息。这里我们看到了nm命令的输出默认有三列其中最左边的一列是变量的相对地址中间的一列表示变量所在的段的类型右面表示变量的名字。我们来分别看一下这三列的意思。 首先最左边这一列是变量在所在段的相对地址我们看到 g_a 和 g_c 的相对地址是相同的这并不冲突因为他们处于不同的段中 D 和 b 表示它们在目标文件中处于不同的段中)。第二列表示变量所处的段的类型比如我们这里看到了有 D,C,b,T 这些类型的段实际上编译器支持的段类型比这个还多。我们同样不去深究各个段类型的意思只要明白不同的段存放的是不同的数据即可比如 D 段就是数据段专门存放有初始值的全局变量T 段表示代码段所有的代码编译后的指令都放到这个段中。在这里我们可以注意到同一个段中的变量相对地址是不能重复的。第三列表示变量的名字这里我们看到局部的静态变量名字被编译器修改为 s_a.1597我们应该能猜得到编译器这么做的原因。s_a 是一个局部静态变量作用域限制在定义它的代码块中所以我们可以在不同的作用域中声明相同名字的局部静态变量比如我们可以在sum函数中声明另外一个 s_a。但是我们上面提过局部静态变量属于全局变量的范畴它是存在于程序运行的整个生命周期的所以为了支持这个功能编译器对这种局部的静态变量名字加了一个后缀以便标识不同的局部静态变量。细心的读者应该能看到为什么这里的变量声明 g_x 没有地址呢我们在C源码文件部分曾经提到过变量和函数的声明本质上是给编译器一个承诺告诉编译器虽然在本文件中没有这个变量或者函数定义但是在其他文件中一定有所以当编译器发现程序需要读取这个变量对应的数据但是在源文件中找不到的时候就会把这个变量放在一个特殊的段段类型为 U里面表示后续链接的时候需要在后面的目标文件或者链接库中找到这个变量然后链接成为可执行二进制文件。 从上面这些信息的解释其实我们可以看出来编译器没有那么复杂它做的任何事情都是为了支持语言级别的功能。 目标文件的链接 通过上一个部分的一个小程序我们讨论了 C 源码文件的基本组成部分编译器对这些组成部分的处理以及编译器这么做背后的原理。同时我们也留下了一个需要在其他目标文件中寻找的变量名。这一小节我们讨论一下在编译器把各个 C 源代码文件编译成目标文件之后链接器需要对这些目标文件做什么样的处理。 首先我们尝试一下对上一小节得到的目标文件链接一下看看有什么结果gcc test.o -o test test.o: In function main: test.c:(.text0x2c): undefined reference to g_x collect2: ld returned 1 exit status当我们尝试把这个目标文件进行链接成为可执行文件时链接器报错了。因为我们之前通过变量声明承诺过的变量并没有在其他的目标文件或者库文件中找到所以链接器无法得到一个完整可执行程序。我们尝试用另外一个 C 程序修复这个问题 int g_x 100; int sub() {}把这个文件编译成目标文件gcc -c test2.c -o test2.o; nm test2.o 0000000000000000 D g_x 0000000000000000 T sub现在我们尝试把这两个目标文件链接成为可执行文件gcc test.o test2.o -o test; nm test, 这时我们发现输出了比目标文件多很多的信息其中定义了很多为了实现不同语言级别的功能而需要的段在这里我们关心的是源文件中定义的那些变量对应的 symbol 及其地址如下图所示 00000000004005e8 T _fini 0000000000400390 T _init 00000000004003d0 T _start ... 0000000000601018 D g_a 0000000000601038 B g_b 0000000000601030 b g_c 000000000060101c D g_x 00000000004004c8 T main 0000000000601034 b s_a.1597 0000000000400504 T sub 00000000004004b4 T sum在最终的可以执行文件里面我们可以看到首先之前在第一个源文件中声明的变量 g_x 和声明的函数 sub 最终在第二个目标文件中找到了定义其次在不同目标文件中定义的变量比如 g_a, g_x 都会放在了数据段中(段类型为 D)还有之前在目标文件中变量的相对地址全部变成了绝对地址。 所以我们再一次进行总结一下链接器需要对源代码进行的处理 对各个目标文件中没有定义的变量在其他目标文件中寻找到相关的定义。把不同目标文件中生成的同类型的段进行合并。对不同目标文件中的变量进行地址重定位。这也是链接器所需要实现的最基本的功能。 装载运行 上面的几个小节中我们讨论了编译器把一个 C 源码文件编译成一个目标文件需要做的最基本的处理也讨论了链接器把多个目标文件链接成可执行文件时需要具备的最基本的功能。在这一个小节我们来讨论一下可执行文件如何被系统装载运行的。 动态链接库 我们都知道在我们写程序的过程中不会自己实现所有的功能一般情况下会调用我们所需要的系统库和第三方库来实现我们的功能。在上面两个小节的示例代码中为了说明问题的简单起见我们仅仅声明定义了几个变量和函数并没有使用任何的库函数。那么现在假设我们需要调用一个库函数提供的功能这个时候可执行文件又是什么样的呢我们再看一个小例子 #include stdio.h #include string.hint main(int argc, char* argv[]) {char buf[32];strncpy(buf, Hello, World\n, 32);printf(%s,buf); }我们把这个文件编译成可执行文件并且查看一下它的symbolsgcc test3.c -o test3; nm test3: 00000000004005b4 T mainU printfGLIBC_2.2.5U strncpyGLIBC_2.2.5我们应该能看到类似上述的输出我们在“目标文件”这一小节曾经看到过这种类型的 symbol。当时是在目标文件中同样也是没有地址我们说这是编译器留给链接器到后面的目标文件中寻找变量定义的。但是现在我们检查的是可执行文件为什么可执行文件里面仍然有这种没有地址的 symbols 呢 我们前面提到过编译器没有什么特别的它做的所有事情都是为了支持编程语言级别的功能这里同样不例外。这里可执行文件中的“未定义”的 symbols 其实是为了支持动态链接库的功能。 我们先来回顾一下动态链接库应该有一个什么样的功能。所谓动态链接库是指程序在运行的时候才去定位这个库并且把这个库链接到进程的虚拟地址空间。对于某一个动态链接库来说所有使用这个库的可执行文件都共享同一块物理地址空间这个物理地址空间在当前动态链接库第一次被链接时 load 到内存中。 现在我们看一下二进制文件中对动态链接库中的函数怎么处理的objdump -D test3 | less搜索printf我们应该能看到以下内容 0000000000400490 strncpyplt:400490: ff 25 6a 0b 20 00 jmpq *0x200b6a(%rip) # 601000 _GLOBAL_OFFSET_TABLE_0x18400496: 68 00 00 00 00 pushq $0x040049b: e9 e0 ff ff ff jmpq 400480 _init0x20 ... 00000000004004b0 printfplt:4004b0: ff 25 5a 0b 20 00 jmpq *0x200b5a(%rip) # 601010 _GLOBAL_OFFSET_TABLE_0x284004b6: 68 02 00 00 00 pushq $0x24004bb: e9 c0 ff ff ff jmpq 400480 _init0x20我们看到可执行文件中为 strncpy 和 printf 分别生成了三个代理 symbol然后代理 symbol 指向的第一条指令就是跳转到_GLOBAL_OFFSET_TABLE_这个 symbol 对应的代码段中的一个偏移位置而在 linux 中这个_GLOBAL_OFFSET_TABLE_对应的代码段是为了给“地址无关代码”做动态地址重定位用的。我们提过动态链接库可以映射到不同进程的不同的虚拟地址空间所以属于“地址无关代码”链接器把对这个函数的调用代码跳转到程序运行时动态装载地址。 Linux 提供了一个很方便的命令查看一个可执行文件依赖的动态链接库我们查看一下当前可执行文件的动态库依赖情况ldd test3: linux-vdso.so.1 (0x00007fff413ff000)libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe202ae7000)/lib64/ld-linux-x86-64.so.2 (0x00007fe202eb2000)ldd 命令模拟加载可执行程序需要的动态链接库但并不执行程序后面的地址部分表示模拟装载过程中动态链接库的地址。如果尝试多次运行 ldd 命令我们会发现每次动态链接库的地址都是不一样的因为这个地址是动态定位的。我们平常工作中如果某一个二进制可执行文件报错找不到某个函数定义可以用这个命令检查是否系统丢失或者没有安装某一个动态链接库。 我们在上面的小程序最后加一个sleep(1000);然后查看一下运行时的内存映射分配cd /proc/21509 cat maps应该可以看到下面这一段 7feeef61f000-7feeef7d4000 r-xp 00000000 fd:01 135891 /lib/x86_64-linux-gnu/libc-2.15.so 7feeef7d4000-7feeef9d3000 ---p 001b5000 fd:01 135891 /lib/x86_64-linux-gnu/libc-2.15.so 7feeef9d3000-7feeef9d7000 r--p 001b4000 fd:01 135891 /lib/x86_64-linux-gnu/libc-2.15.so 7feeef9d7000-7feeef9d9000 rw-p 001b8000 fd:01 135891 /lib/x86_64-linux-gnu/libc-2.15.so我们可以看到进程运行时系统为 libc 库在进程地址空间中映射了四个段因为每个段权限不同所以不能合并为一个段。对这些动态链接库的调用最终会跳转到这里显示的地址中。 根据以上这些信息我们在这里继续总结一下链接器需要对动态链接库需要做的最基本的事情 链接库在将目标文件链接成可执行文件的时候如果发现某一个变量或者函数在目标文件中找不到会按照 gcc 预定义的动态库寻找路径寻找动态库中定义的变量或者函数。如果链接库在某一个动态链接库中找到了该变量或者函数定义链接库首先会把这个动态链接库写到可执行文件的依赖库中然后生成这个当前变量或者函数的代理 symbol.在_GLOBAL_OFFSET_TABLE_代码中生成真正的动态跳转指令并且在库函数比如strncpy,printf代理symbol中跳转到_GLOBAL_OFFSET_TABLE_中相应的偏移位置。前面我们一直在讨论动态链接库so库其实在各个平台下面都有静态链接库静态链接库的链接行为跟目标文件非常类似但是由于静态库有一些问题比如因为每个可执行文件都有静态库的一个版本这导致库升级的时候很麻烦等问题现在静态库用的非常少所以这里我们不去深究。 main函数之前 在“操作系统”这一小节中我们曾简单提过在程序的 main 函数执行之前进程需要做一些初始化工作然后才会调用 main 函数执行程序逻辑。在“动态链接库”在这一小节中我们提到了对于动态链接库我们需要在系统启动的时候把需要的库动态链接到进程的地址空间。在本节中我们综合这些步骤从可执行文件的目标代码中简单跟踪一下Linux 是如何把 elf 文件 load 到内存中并且最终调用到 main 函数的。 在“目标文件的链接”这一小节中我们展示了部分nm test的结果其中_start这个 symbol 是故意被留下来的因为对于 elf 文件格式来说linux 系统在为进程分配完虚拟地址空间并且把代码 load 到内存之后是从这_start对应的地址开始执行的。这个地址记录在 elf 文件的头中系统读取 elf 文件时可以得到这个地址。下面我们就从_start这个 symbol 对应的指令开始并追踪一下我们感兴趣的关键点。 0000000000400510 _start:...400526: 48 c7 c1 70 06 40 00 mov $0x400670,%rcx40052d: 48 c7 c7 f4 05 40 00 mov $0x4005f4,%rdi400534: e8 b7 ff ff ff callq 4004f0 __libc_start_mainplt /*.start这个段会去执行libc库中的__libc_start_main的指令这里需要注意一下传给这个函数的两个参数值“0x400670”和“0x4005f4”其中一个是__libc_csu_init的地址一个是main函数的地址*/...00000000004004f0 __libc_start_mainplt:4004f0: ff 25 22 0b 20 00 jmpq *0x200b22(%rip) # 601018 _GLOBAL_OFFSET_TABLE_0x300000000000400670 __libc_csu_init:...4006b0: e8 e3 fd ff ff callq 400498 _init...0000000000400498 _init:...4004a6: e8 65 02 00 00 callq 400710 __do_global_ctors_aux...00000000004005f4 main:...400626: e8 95 fe ff ff callq 4004c0 strncpyplt...40063f: e8 9c fe ff ff callq 4004e0 printfplt...400649: e8 b2 fe ff ff callq 400500 sleepplt...我们先来简单解释一下上述贴的几段指令的意思首先在 _start 对应的指令中经过一些处理之后会用__libc_csu_init的地址和main的地址作为参数调用__libc_start_main这个函数是在libc库中实现的也就是linux中所有的可执行程序都共享同一段初始化代码篇幅原因我们不去查看__libc_start_main的实现了。我们需要知道的是在__libc_start_main作为一些处理之后会先调用__libc_csu_init对应的指令然后调用main对应的指令。 main对应的指令就是我们自己的main函数了__libc_csu_init接着会调用_init的指令然后会调用__do_global_ctors_aux这个 C 程序员都应该熟悉的 symbol 对应的指令__do_global_ctors_aux对应的指令会进行所有的全局变量初始化或者 C 中的全局对象构造等操作。 根据上述信息我们总结一下当我们通过bash运行一个程序的时候Linux 做了哪些事情 首先 bash 进行 fork 系统调用生成一个子进程接着在子进程中运行 execve 函数指定的 elf 二进制程序 Linux中执行二进制程序最终都是通过 execve 这个库函数进行的execve 会调用系统调用把 elf 文件 load 到内存中的代码段(_text)中。如果有依赖的动态链接库会调用动态链接器进行库文件的地址映射动态链接库的内存空间是被多个进程共享的。内核从 elf 文件头得到_start的地址调度执行流从_start指向的地址开始执行执行流在_start执行的代码段中跳转到libc中的公共初始化代码段__libc_start_main进行程序运行前的初始化工作。__libc_start_main的执行过程中会跳转到_init中全局变量的初始化工作随后调用我们的main函数进入到主函数的指令流程。 至此我们讨论了从一个 C 语言程序的源代码到运行中的进程的全过程。 ###一个小例子 在明白了编译器如何把我们的源代码“转变”成二进制可执行程序之后我们就能够知道怎么样去看某一段代码被编译成二进制之后是一个什么样子然后就可以按照编译器的“习惯”写出高效的代码。 这一小节我们分析一个网上的小例子下面是一个网友列出的两段程序在面试中被问到各有什么优缺点。 程序1 ifk 8{for (int h0;h100;h) { //doSomething } } else {for (int h0;h100;h) { //doSomething } }程序2 for (int h0;h100;h) {if (k8) { //doSomething } else { //doSomething } }从编程规范上看很明显程序2 是好于程序1 的因为如果“doSomething”的部分比较复杂程序2 紧凑而不冗余而且可以把 if 和 else 分支“doSomething”公共的部分提取出来放在 for 循环下面。但是有经验的工程师马上也能看出来虽然程序1稍显冗余但是其执行速度比程序2 是要快的为什么快我们从编译器生成的目标文件分析一下我们的测试程序如下 程序1 if(type 0) {for(i0; icnt; i) {sum data[i]; } }else if(type 1) {for(i0; icnt; i) {sum (i0x01)?(-data[i]):data[i];} } 程序2 for(i0; icnt; i) {if(type 0) {sum data[i];}else {sum (i0x01)?(-data[i]):data[i];} } 编译成可执行文件后的片段为 程序1 4005d7: 83 7d ec 00 cmpl $0x0,-0x14(%rbp) /* type0判断 */ 4005db: 75 29 jne 400606 calc_10x44 /* 条件判断失败则跳到else分支 */ 4005dd: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) ------------------------------循环体开始--------------------------- 4005e4: eb 16 jmp 4005fc calc_10x3a /* 跳到循环条件比较指令 */ 4005e6: 8b 45 fc mov -0x4(%rbp),%eax /* 循环内第一条指令 */... 4005f8: 83 45 fc 01 addl $0x1,-0x4(%rbp) 4005fc: 8b 45 fc mov -0x4(%rbp),%eax 4005ff: 3b 45 e8 cmp -0x18(%rbp),%eax /* 循环条件比较 */ 400602: 7c e2 jl 4005e6 calc_10x24 /* 跳到循环开始阶段 */ ------------------------------循环体结束---------------------------- 400604: eb 4a jmp 400650 calc_10x8e 400606: 83 7d ec 01 cmpl $0x1,-0x14(%rbp) /* else分支type1判断 */ ... /* type1 分支基本与type0的分支是一致的 */ 程序2 400671: eb 4d jmp 4006c0 calc_20x6b ------------------------------循环体开始--------------------------- 400673: 83 7d ec 00 cmpl $0x0,-0x14(%rbp) /* type0 */ 400677: 75 14 jne 40068d calc_20x38 /* 条件判断失败则跳到else分支 */... 400686: 8b 00 mov (%rax),%eax 400688: 01 45 f8 add %eax,-0x8(%rbp) 40068b: eb 2f jmp 4006bc calc_20x67 40068d: 8b 45 fc mov -0x4(%rbp),%eax /* else分支 */ 400690: 83 e0 01 and $0x1,%eax 400693: 84 c0 test %al,%al 400695: 74 13 je 4006aa calc_20x55 ... 4006c0: 8b 45 fc mov -0x4(%rbp),%eax 4006c3: 3b 45 e8 cmp -0x18(%rbp),%eax /* 循环条件比较 */ 4006c6: 7c ab jl 400673 calc_20x1e /* 跳到循环开始阶段 */ ------------------------------循环体结束---------------------------- 通过对比源程序和汇编指令我们程序1和程序2 编译完之后的汇编指令分别进行了对比标注。我们可以对比一下在程序1 的汇编指令中经过一次条件判断之后执行流会跳到相应的循环指令段中if/else然后循环执行整段的指令。而在程序2 的汇编指令中在每一次执行循环指令段的过程中都有条件的判断和跳转if/else。 所以这里我们可以总结一下程序2 比程序1速度快的原因 程序2 中每次循环体的执行都需要执行比较指令和跳转指令如果循环次数非常多比如大于百万次就相当于多执行百万条指令。现代的 CPU 都是流水线模式模式也有指令预取模块也就是说同一时间段内有多条指令在 CPU 内运行同时也有预测的指令预取。如果发生了指令跳转就很有可能造成后续的指令全部被刷出 CPU重新跳转到新地址执行浪费多个 CPU 周期。通过我们的分析我们可以说如果此程序段处于整个项目中非瓶颈的位置程序2作为优先选择的是可以接受的。但是如果此程序段处于速度瓶颈位置程序1是占有优势的。 结束语 本文中我们使用 C 语言为例串讲了源代码程序如何经过编译链接和装载最终成功在 Linux 系统运行起来。从代码细节上看这是一个漫长复杂的过程但是只要抓住其中的主线就会发现其实编译器和链接器所做的时候都是为了满足我们的功能和需求正所谓万变不离其宗。 另外虽然我们使用 C 语言进行说明的在同一种系统中其他的语言编译得到的二进制文件是一样的格式的。比如在 Linux 下面使用 go 语言编译的源代码最终编译出来的二进制文件仍然是elf格式的我们可以使用同一套工具比如 nm,objdump,readelf查看和调试这样的代码。由此我们可以得知虽然go的编译器和gcc编译器细节实现上有所不同但所做的工作基本是一样的。 显然这样一篇短文不可能很详尽的把编译链接和装载这么复杂的过程描述的很细致。本文的初衷是为了让同学们对这个过程有一个直观的了解有兴趣的同学其实还有大把的细节可以去探索最后的“参考”小节中有几个不错的资源可以为有兴趣的同学提供参考。 参考 俞甲子石凡潘爱民: 程序员的自我修养http://www.lurklurk.org/linkers/linkers.htmlJohn Levine: Linkers and Loaders
http://www.zqtcl.cn/news/193923/

相关文章:

  • 网站建设与管理视频网站推广的方法枫子
  • 苏州市住房和城乡建设局官方网站宠物之家网站开发
  • 建个人网站活字格能开发企业网站吗
  • php网站后台密码忘记做电子商务网站 语言
  • 网站建设策划师怎样进入国外网站
  • 建设银行商城网站浙江建站管理系统价格
  • 我想做个网站怎么做的常用的网络营销方法及效果
  • 南通专业做网站南宁网站建设mxfsem
  • 阿里巴巴电子商务网站建设目的网站专题素材
  • 浙江虎霸建设机械有限公司网站哪个网站做简历好
  • 网站做电商资质吗网站开发作品
  • 大型彩灯制作公司临清聊城网站优化
  • 网站建设灬金手指下拉十五网络运维工程师简历怎么写
  • 黄岛建设局网站动漫采集WordPress
  • 做网站现在挣钱吗wordpress 网址导航主题
  • 外贸网站什么采集wordpress主题更换logo
  • 唐山开发网站的公司长沙营销型网站设计
  • 数据库策略网站推广的有效方法有美辰网站建设
  • c 网站开发构想做网站的点子
  • 个人网站模板下载提供网站建设备案公司
  • 做网站需要会写代码6山东东营
  • 兼职刷客在哪个网站做网站搬家数据库配置
  • 做搬运的话哪个网站好网站模板建站
  • 建设个人信息网站wordpress 用户权限
  • 网站不显示域名解析错误怎么办公益网站设计
  • 怎么上传网站图片的链接手表网站排行榜
  • 网站推广方法100种百度排名规则
  • 上海专业网站建设公司站霸网络萝岗区网站建设推广
  • 做微商网站的公司永久免费crm管理系统
  • 网站开发的环境专业的建设网站