程序“编译/运行“错误分析原理

        最近总结了从程序运行底层的角度分析程序报错,在此分享一下,希望能帮助到跟我有同样需求的小伙伴们。如果有理解不到位的还请小伙伴们指点指点~~

        本篇文章从宏观上能让你了解这样一些大致原理:应用程序执行环境、程序从编写到编译运行经历的过程、目标文件大致内容;并且能解决你这样一些疑问:为什么应用程序不能兼容运行在不同的操作系统上?安卓手机上的应用程序为什么就不能在同是linux内核的pc端上跑?为什么有些程序编译的时候对编译器版本有要求?为什么操作系统升级之后某些库或者应用就用不了了呢?不同种类的编译器编译出的二进制库可以相互链接吗?;最重要的是能让你学会分析解决程序从编写到编译运行过程中遇到的绝大多数问题。

一、应用程序对平台的兼容性

1、应用程序运行环境

        应用程序执行环境如下图1所示,应用程序是运行在操作系统之上的,而操作系统是运行在计算机硬件之上的,应用程序不能直接访问计算机底层的硬件资源,而是需要调用操作系统提供的API(叫做系统调用)间接访问底层的硬件资源。那么操作系统又是怎么控制计算机底层的硬件呢?这就不得不提到指令集了(ISA,Instruction Set Architecture),指令集是专门用于控制cpu内部资源(也就是控制器、运算器、寄存器等这些)的一套指令的集合(其实就是一些二进制代码),每款CPU在设计时就规定了与之相应的指令集,所以指令集是由cpu生产厂家提供的。因此操作系统是通过调用这些指令集中代码的组合来控制cpu的。

 图1 应用程序运行环境

2、指令集对程序执行的影响                       

        因此操作系统的运行是依赖于cpu的指令集的,一般不同种类的cpu的指令集是不一样的,指令集越复杂那么cpu性能就越高(因为可以将很多复杂的逻辑运算直接通过硬件电路来实现)。比如常见的手机上的cpu指令集跟pc电脑上的指令集是不同的,手机上的cpu由于性能要差一些,因此它的指令集不能太过于复杂,而pc电脑上的cpu体积比较大、散热性比较好,因此可以采用比较复杂的指令集。

3、应用程序对硬件平台的依赖性

        应用程序执行时候的二进制代码集合其实就是指令集中各元素的组合,因此即使安卓手机和同是linux内核的pc电脑的内核都是linux(其实它们的linux内核还是不一样的),由于它们的指令集不同(也叫cpu架构不同),应用程序在这两个平台上跑的二进制代码集合是不一样的,因此安卓手机上的应用程序就不能在同是linux内核的pc端上跑。

二、应用程序对操作系统的兼容性

1、目标文件

        目标文件也就是操作系统最终可以直接运行的文件,一般Linux的目标文件以.so、.out为后缀或者根本没有后缀的文件,Windows系统的目标文件一般以.dll或者.exe为后缀的文件,目标文件的内容大概是长这样子的(如下图2所示的目标文件A所示)。

图2  目标文件内容及其动态链接、加载过程

        如图2的目标文件A所示,主要由两部分组成:文件头和文件体。文件头主要是放一些描述性信息,并没有程序执行的相关代码,但是其中有一张符号表非常重要(linux中好像叫做SHT表),这张表可以简单的理解为文件中存放的函数名及其对应的偏移地址,如果外部程序要动态链接这里面某个函数的代码段,只需要根据函数名称在这张表里面找到其代码段的偏移地址就行了。文件体可以简单的理解为存放可执行代码的地方。不同格式的目标文件它们的文件头和文件体一般是有差异的。

        如果目标文件需要链接其他目标文件中的代码,那么在文件体中还会有一个叫做ABI的东西(Application Binary Interface)。ABI可以先简单地跟API(Application Programming Interface)对应起来,API是源文件的调用接口,而ABI是二进制层面上的调用接口,比如图2中目标文件A需要调用目标文件B中的函数func2。但是为了实现这种调用,还需要明确很多事儿,比如函数表要怎么查找,函数参数怎么传,返回值放哪里,栈由谁清理等这些硬件底层具体的操作也是需要ABI描述清楚的,而这些具体描述是由编译器和操作系统共同决定的(操作系统规定描述的格式框架,编译器在这个框架下决定具体怎么做)这个在后续讲链接器的时候会介绍。

2、链接器扮演的角色

        链接器其实就是一个系统级的应用程序,linux中的链接器为/lib/ld-linux.so.*,链接分为静态链接和动态链接两种,而动态链接分为load-time和run-time,load-time是在应用程序加载进主存时产生的链接,run-time是应用程序运行时产生的链接,后续只介绍load-time链接,run-time链接原理可以举一反三区别不大。

        链接器的作用主要有三个:加载依赖文件到主存中符号决议以及重定位

        1)加载依赖文件到主存中

        如图2的主存示意图所示,如果是load-time类型的动态链接,那么在程序执行之前需要将整个依赖的目标文件B加载到主存中。

        2)符号决议

        加载完所有的依赖文件到主存中之后,查看主程序是否还有未实现的接口代码,如果有那么会报出类似于`undefined referenceXXX`这样的错误。为了减轻链接器查找函数符号的工作量,编译器生在目标文件的符号表中会列出该文件哪些函数符号是已经实现的,哪些函数符号是未实现需要调用其他文件中的函数;链接器就可以根据这张符号表快速的找到我需要的函数代码。

        3)重定位

        在依赖文件加载到主存中之后需要更新依赖文件中代码在主存中的位置(猜测是更新ABI中的参数),这样应用程序(ABI)调用它们时可以准确的找到这些代码。

3、目标文件执行过程

        对于load-time类型的动态链接,目标文件的执行过程为:

操作系统开辟一个进程空间->加载主程序的目标文件到主存->加载链接器的目标文件到主存->然后运行链接器的目标代码将依赖文件加载到主存中去->开始执行主程序

        如图2所示,操作系统首先创建一个进程空间,然后将目标文件A加载到主存中去,然后加载链接器到主存中去,然后运行链接器代码加载依赖文件B到主存中去(包含符号决议和重定位),最后才开始执行主存中A的代码。

4、应用程序对操作系统的依赖性

        应用程序其实就是一系列的目标文件以及数据文件组成的集合,不同的操作系统对于目标文件的格式规范是不同的,这主要体现在链接器能够识别目标文件格式。比如linux的链接器能识别ELF格式的文件,windows能识别PE格式的文件。因此即使是同一个硬件系统,不同的操作系统上的应用程序也是不能相互兼容的。

        另外操作系统的具体版本也会对应用程序的兼容性产生影响。比如在操作系统的某一个版本上发布了一个应用程序,操作系统升级之后可能会更新一些系统调用的API接口,导致应用程序找不到相应的系统依赖。

三、编译器对应用程序的影响

1、认识编译器

        编译器其实就可以理解成一个翻译软件,将我们人类能够识别的高级语言翻译成二进制低级语言。编译器首先是跟硬件先天相关的,因为编译器的目标之一就是将源文件中的代码翻译成指令集中元素组合的二进制代码;同时编译器跟操作系统是后天相关的,因为编译器也是操作系统当中的一个程序而已,它的运行环境也是依赖于操作系统的,其次编译器生成的目标文件格式是由操作系统指定的,这些目标文件要能被操作系统识别。

2、应用程序的生成

        应用程序的生成分为预处理、编译、汇编、链接等过程,具体过程本文不进行细讲,这里讲一下编译器对函数名编码的问题。我们知道链接器确定被调用函数的地址时,在符号表中会根据函数名查找函数的地址,而函数名有些时候是被重载的(比如c++),那怎么才能保证在符号表中的函数名唯一呢?

        对于c语言来说这种问题是不存在的,因为c语言不支持函数的重载,但是对于c++语言来说就存在这个问题。c++语言中的解决方式就是将函数名和所有的参数类型一起编码,形成一个新的函数名,然后符号表中就可以这个函数名唯一了。

3、编译器对应用程序的影响

        编译器对应用程序的主要影响体现在对函数名的编码算法上,如果编译器的函数名编码算法是一样的,那么两个目标文件是可以相互兼容的(相互链接成功),否则不兼容。比如图2所示的目标文件A是由编译器a生成的,目标文件B是由编译器b生成的,如果编译器a和b采用了不同的函数名编码算法,那么目标文件A中的函数名是不会存在目标文件B中的符号表中的,也就导致链接器链接不上。

        因此不同种类的编译器编译出的二进制库是否可以相互链接,得要看它们的函数名编码算法是否一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值