A没问题,B没问题,A+B却出问题了
A指的是,我处理C/C++静态库lib文件,将其加载到内存,分析其二进制格式,提取其中的各种数据信息(obj,段(Section),符号(Symbol),重定位(Relocation),字符串表(StringTable) 等)。
B指的是,我读入LIB文件,改写和调整其中的部分数据,再写出新的LIB文件。在改写数据之前,要先加载并分析,重用A是很自然的事情。
A这一部分功能,一直在测试,一直在正常使用,一直没问题。B这一部分,代码被仔细检查了好多遍,也不应有问题了。可是为什么到最后,生成的LIB文件不合法呢(不被链接器认可)?经过漫长而坚苦的排错过程,发现最终导致问题出现的根源是:A中会修改加载到内存中的个别字节数据(修改后就不再是合法的LIB数据了),而B是在A修改过的内存的基础上处理并输出的。
其实从A的角度说,对内存中的LIB文件数据进行修改是完全合理的,因为它的任务只是提取LIB中的信息,达到它的目的就OK了,具体是怎么实现的那是内部细节问题,况且它并未修改作为输入数据的LIB文件。A出现的时候,还没有B,A不需要为B的使用问题负责。从B的角度说,它做了很多工作,并且保证了自己的工作没有失误,这应该足够了吧。它的代码没问题啊。
A和B从单方面说都没有问题,两者集成到一起,就出问题了。这就是很无奈的结局。事后仔细分析,A、B两者都有责任,B要负更大的责任。B没有检查和确认它的输入数据,想当然的以为它是合法的输入。A的责任是,修改内存中的输入数据这一作法,比较隐晦,是不安定的因素,有潜在的导致出错的苗头,应有意避免。
事后我重读A中那段代码,发现有注释中明确指出了这里会修改内存中的输入数据。说明当时就已经意识到这段代码是不安定的因素。可是最终还是这么做了,为什么呢?这要从头说起了。在LIB文件格式(Unix Archive格式)中,每个OBJ都有一个对应的ArchiveMemberHeader结构体,另有三个特殊的Member(First Linker Member, Second Linker Member, Longnames Member)也都有这么个结构体,这些结构体中存储着对应Member的基本信息,其中第一个成员(Name)存储该Member的名称,其数据结构为16字节的char数组。根据文档中的规定,这个16字节的char数组,其数据内容有以下几种形式:/,//,xxxx/,/n,不足16字节后面用空格填充。注意,这里没有C字符串结尾符'\0',要返回这个name,要么malloc一块内存填充后返回,要么就写一个'\0'进去并返回首地址。考虑到更少的内存占用和更快的执行效率,我(liigo)选择了后者。分析name的几种情况:前两种长度是固定的,后面肯定有多个空格,把第一个空格改写成'\0'就OK;第三种情况,最后的那个标识结束的/字符恰好不属于名称的一部分,把它改写成'\0'就OK,不用担心超出16字节的边界;第四种情况,n表示Longnames区中字符串表的偏移,那里本来就是以'\0'结尾的字符串,不需要特殊处理,只要根据n计算出文本首地址即可。总之以上做法避免了内存申请和数据复制,是比较合理的选择,虽然有一些争议。
最后是怎么解决问题的呢?我还是保留了A中的做法,选择了在B中把A曾经改过的数据再改回去。呵呵,我宁愿事后再改回去,也不愿意放弃先前的修改。毕竟之前已经做过决策,评估过优劣,没必要事后予以否定。况且现在选择的做法,也不比修改之前的代码麻烦。