《计算机组成原理/CSAPP》网课总结(二)——编译原理基础

这部分是四月份的安排,拖到五一放假了,主要是对源码编译过程的一次总结,总的来说,大致可分为预编译、编译、汇编和链接四部分。这里简单记录一下:

《计算机组成原理/CSAPP》网课总结(二)——编译原理基础

一 概述

  • 1、预处理
    或者说是预编译,指的是在编译前需要做的一些处理,如宏替换、include替换等等,这部分没什么东西
    每一个.c或.cpp源代码文件会生成一个对应的.i文件;
  • 2、编译
    编译过程将预处理后的文件生成为.s的汇编文件,汇编文件可用文本编辑器打开查看,里面的汇编代码是直接对应CPU动作的;
  • 3、汇编
    汇编过程将.s汇编文件映射为可重定位目标文件, 一般为.o或.obj扩展名。
  • 4、链接
    链接阶段是通过链接器将不同的.o文件进行打包,可以理解为单纯的拼接操作,但操作的时候会检查各个实现是否存在。此外,链接可执行文件时还会导入c或cpp的启动相关的必要系统文件,如cruntime等

二 其他相关知识点

  • 在shell中启动可执行文件后,shell会调用操作系统的加载器将可执行文件读入内存,然后将cpu的控制权交给可执行文件,然后开始执行。

  • 可重定位目标文件的基本组成部分如下,链接过程是基于符号完成粘合的,比如a文件中调用了函数f,b文件中定义了函数f,那么链接过程就能正确完成;否则会出现找不到定义的链接错误,同样如果出现重复定义,也会报错。

  • 至于最终生成可执行文件还是库文件,取决于程序员。如果是生成可执行文件,链接器还会链接调用main函数的相关系统文件,这些文件会调用main函数,所以如果源码里面没有实现,就会报“无法解析的外部符号main”,因为连接器找不到main函数的实现。

  • 如果是生成库文件,比如静态库,我们在linxu下可以用命令ar rcs libstatic.a *.o *.o,这样可以将可重定位目标粘合在一起,在使用的时候,我们只需要include静态库的头文件,使用其中的函数声明,然后再静态链接的时候链接之前生成的libstatic.lib即可,这个时候只有那些使用到的函数定义会被复制到可执行文件中,没有使用的不会复制,当然其他cruntime模块肯定会被链进来,这是默认的。

  • 如下图是一个可执行文件所包含的模块,主要分为代码段和数据段,以及其他部分,程序执行时前两部分会加载到内存中,然后跳转到系统函数_start()处开始执行,_start()函数是C运行时库中定义的,然后_start()调用_libc_start_main(),然后再调用用户代码中的main()函数

《计算机组成原理/CSAPP》网课总结(二)——编译原理基础
  • 如下图是可执行文件加载到内存中虚拟地址空间布局,我们在平时写代码的时候,需要理解其中的不同区域的意义:
    在linux x86-64系统中,代码段总是从0x400000处开始,这部分是只读的;然后是数据段;接下来是堆内存段,堆内存是从低地址向高地址分配的;然后是一部分为共享库保留的内存区域;然后是栈空间,起始地址是248-1,这是最大合法用户地址,栈的开辟方向是从高到低;在往上,从248开始的地址是为操作系统的代码和数据保留的,对用户代码不可见。
《计算机组成原理/CSAPP》网课总结(二)——编译原理基础
  • 在启动可执行文件后,操作系统会使用地址空间随机化的策略,栈、共享库和堆的运行时地址都会变化,以防止受到攻击。

  • 动态链接库(共享库),一种特殊的可重定位目标文件。生成共享库的方式和静态库类似,linux下编译命令为gcc -shared -fpic -o libshared.so A.c B.c,这样就可以生成位置无关的动态链接库文件,在使用时,通过命令gcc -o main main.c ./libshared.so完成动态链接,这个动作只会复制符号表和重定位信息。值得一提的是,在windows下,动态库的符号表和可重定位信息单独存放在一个.lib的动态库的导入库文件中,而真正的动态库实现在另一个同名的.dll中,所以在Windows下执行动态链接其实是静态链接导入库的过程。

  • 动态链接库的使用,在可执行文件启动时,可执行文件会检查一个名为.interp的section, 里面包含了动态链接器(ld-linux.so)的路径名,启动动态链接器来执行重定位代码和数据的工作,将动态链接的共享库加载到某个内存段,然后重定位可执行文件中由动态库定义的符号引用。完成重定位后再将控制权交还给可执行文件,至此完成动态库的加载和重定位工作,以后动态库的内存位置就固定了。这种动态库方式需要在编译时就链接动态库,在可执行文件开始执行前就要完成加载,这种方式称为动态库的静态加载

  • 动态库的动态加载,这种技术更加灵活,无需再编译期将动态库链接到应用中,在运行期间加载某个共享库进行使用。linux下可使用void *dlopen(const char *filename, int flag)进行运行期加载动态库,示例:void* handle = dlopen("./libvector.so", RTLD_LAZY),信号RTLD_LAZY意思是推迟符号解析直到动态库中的代码被调用时。使用动态库的函数的方法:void *dlsym(void* handle, char* symbol),比如说我们蒂阿勇句柄handle指向的共享库中的add(int a, int b)函数,那么add = dlsym(handle, "add")将返回函数add(int a, int b)的地址供使用;最后,调用方法int dlclose(void* handle)可以将动态库关闭(卸载)。

以上做了一个简要总结,这些内容在我们写具体代码时可能不太重视,但是对构建知识体系,处理一些链接bug还是非常重要的。

发表评论

相关文章

当前内容话题
  • 0