C/C++ 高级编译教程(四)
十一、动态库:杂项主题
Abstract
在理解了动态库概念背后最深刻的思想之后,在深入了解软件专业人员日常处理库的工具箱的细节之前,现在是仔细研究几个遗留问题的好时机。首先,让我们仔细看看插件的概念,它是无缝扩展框架基本功能的无所不在的机制。然后,我将指出源于动态库概念的一些实际含义。最后,我将仔细看看开发人员在日常工作中可能遇到的一些杂七杂八的话题。
在理解了动态库概念背后最深刻的思想之后,在深入了解软件专业人员日常处理库的工具箱的细节之前,现在是仔细研究几个遗留问题的好时机。首先,让我们仔细看看插件的概念,它是无缝扩展框架基本功能的无所不在的机制。然后,我将指出源于动态库概念的一些实际含义。最后,我将仔细看看开发人员在日常工作中可能遇到的一些杂七杂八的话题。
插件概念
动态链接的进步可能带来的最重要的概念是插件的概念。这个概念本身没有什么难以理解的,因为我们在日常生活中会遇到很多这样的场景,其中大多数不需要任何技术背景。插入式概念的一个很好的例子是钻头和各种钻头,它们可以根据特定情况的需要和最终用户的决定而改变(图 11-1 )。
图 11-1。
Drill and bits, an everyday example of the plug-in concept
插件的软件概念遵循同样的原则。基本上,有一个主要应用程序(或执行环境)对某个处理主题执行某个动作(例如,修改图片属性的照片处理应用程序),还有一组模块专门对处理主题执行非常具体的动作(例如,模糊滤镜、锐化滤镜、棕褐色滤镜、颜色对比度滤镜、高通滤镜、平均滤镜等)。),这是一个非常容易理解的概念。
但这还不是全部。
并不是所有包含旗舰应用程序和相关模块的系统都应该被称为“插件架构”为了让架构支持插件模型,还需要满足以下要求:
- 添加或删除插件不应要求重新编译应用程序;相反,应用程序应该能够在运行时确定插件的可用性。
- 模块应该通过某种运行时可加载机制来导出它们的功能。
- 无论最终用户在运行时可以使用哪些插件,系统都应该是正常运行的。
实际上,上述要求通常通过以下设计决策来支持:
- 插件被实现为动态库。不管内部功能如何,所有插件动态库都导出标准化接口(一组允许应用程序控制插件执行的函数)。
- 应用程序通过动态库加载过程加载插件。通常支持以下两个选项:
- 应用程序查看预定义的文件夹,并尝试加载它在运行时找到的所有动态库。在加载时,它试图找到与插件期望导出的接口相对应的符号。如果没有找到这些符号(或者只找到了其中的一部分),插件库就会被卸载。
- 用户通过运行时的专用 GUI 选项指定插件位置,并告诉应用程序加载插件并开始提供其功能。
出口规则
并不存在针对每一个插件架构的严格规则。然而,确实存在一套常识性的指导原则。根据解释 C++ 语言对链接器问题的影响的段落,大多数插件体系结构倾向于遵循最简单的可能方案,其中插件导出指向由 C 链接函数组成的接口的指针。
尽管插件的内部功能可以实现为 C++ 类,但这样的类通常实现由其动态库容器导出的接口,并且将指向类实例的指针(转换为指向接口的指针)传递给应用程序是惯例。
流行的插件架构
支持插件架构的流行程序种类繁多,例如(但不限于):
- 图像处理应用程序(Adobe Photoshop 等。)
- 视频处理应用(索尼维加斯等。)
- 声音处理(斯坦伯格 VST 插件架构,在所有主流音频编辑器中得到普遍支持)
- 多媒体框架(GStreamer,avisynth)和流行的应用程序(Winamp,mplayer)
- 文本编辑器(其中大多数都有提供特定功能的插件)
- 软件开发集成开发环境(ide)通过插件支持多种功能
- 版本控制系统的前端 GUI 应用程序
- Web 浏览器(NPAPI 插件架构)
- 等等。
对于这些插件体系结构中的每一个,通常都有一个公开的插件接口文档,详细规定了应用程序和插件之间的交互。
提示和技巧
要完全理解动态库的概念,最后一步需要您后退一步,将您到目前为止所学的一切整理成另一组简单的事实。在日常设计实践中,以不同的方式表述事物有时可能意味着很大的不同。
使用动态库的实际意义
在检查了关于动态库的所有细节之后,关于它们最有力的事实是,针对动态库的链接是一种基于承诺的链接。事实上,在构建阶段,客户端可执行程序所担心的只是动态库符号。只有在运行时加载阶段,动态库部分的内容(代码、数据等)才会被加载。)来玩。从所描述的一系列情况中,有几个现实生活中的含义。
条块分割,更快发展
动态库的概念给了程序员很大的自由。只要对客户机可执行文件重要的符号集不变,程序员就可以自由地修改实际的动态库代码,只要需要就可以。
这个简单的事实对编程的日常程序有巨大的影响,因为它会大大减少不必要的编译时间。通过使用动态库,程序员可以减少对动态库本身重新构建代码的需要,而不必在代码发生微小变化时重新编译整个代码。难怪程序员经常决定将正在开发的代码放在动态库中,至少直到开发完成。
运行时快速替换能力
在构建时,客户机二进制文件不需要完全成熟的动态库,所有的功能都已经就绪。相反,客户端二进制文件在构建时真正需要的是动态库的符号集——仅此而已。
这真的很有趣。请深吸一口气,让我们看看这个说法到底是什么意思。
您在构建时使用的动态库二进制文件和在运行时加载的动态库文件可能在每个方面都有很大的不同,只有一点除外:符号必须匹配。
换句话说(是的,这是真的,也正是它的本意),为了静态感知的构建目的,你可以使用动态库,它的代码(flash 和 blood)还没有实现,但是它的符号(骨架)已经处于它们的最终形状。
或者,您可以使用一个您知道其代码会改变的库,只要您确信导出的符号集不会改变。
或者,您可以在构建时使用适合一种特定风格(如语言包)的动态库,但在运行时与另一个动态库链接—只要两个动态库二进制文件导出相同的符号集。
这真的非常非常有趣。我们如何从这一重要发现中受益的一个极端例子发生在 Android 原生编程领域。在开发一个模块(动态库或原生应用程序)的过程中,整个开发团队不必要且不明智地采取耗时的方式将他们的源代码添加到巨大的 Android 源代码树中,这种情况并不少见。
或者,更有效的方法是开发一个模块作为独立的 Android 项目,与 Android 源代码树无关。在几分钟内,完成构建阶段所需的 Android 本地动态库可以从任何工作的 Android 设备/手机中复制(在 Android 行话中称为“adb pulled ”),并添加到项目构建结构中。以前需要几个小时,现在构建过程最多只需要几分钟。
即使从最近的可用 Android 手机中提取的动态库的代码(?? 部分)可能与在 Android 源代码树中找到的代码明显不同,但符号列表在两个动态库中很可能是相同的。显然,从 Android 设备上拉取的快速替换库可能满足构建需求,而在运行时,将加载动态库二进制文件中“正确的那个”。
杂项提示
在本章的剩余部分,我将讲述以下有趣的知识:
- 将动态库转换为可执行文件
- Windows 库的冲突运行时内存处理方案
- 链接器弱符号
将动态库转换为可执行文件
正如在前面关于动态库的介绍性讨论中所指出的,动态库和可执行文件之间的区别在于,后者有启动例程,允许内核实际开始执行。在所有其他方面,特别是如果与静态库相比,似乎动态库和可执行文件具有相同的性质,例如二进制文件,其中所有的引用都已被解析。
鉴于如此多的相似之处和如此少的差异,有可能将动态库转换为可执行文件吗?
这个问题的答案是肯定的。这在 Linux 上肯定是可能的(我还在寻找在 Windows 上证实这一说法)。事实上,实现 C 运行时库(libc.so)的库实际上是真正可执行的。当通过在 shell 窗口中键入文件名来调用时,您会得到如图 11-2 所示的响应。
图 11-2。
Running libc.so as executable file
接下来自然出现的问题是如何实现这个库以使其可执行?
以下食谱使之成为可能:
- 实现动态库中的主函数——这个函数的原型是
int main(int argc, char* argv[];
- 声明标准的
main()函数作为库入口点。传递-e链接器标志是完成这项任务的方法。
gcc -shared -Wl,-e,main -o<libname>
- 将
main()功能变为不返回功能。这可以通过在main()函数的最后一行插入_exit(0)调用来实现。 - 将解释器指定为动态链接器。下面一行代码可以做到这一点:
#ifdef __LP64__
const char service_interp[] __attribute__((section(".interp"))) =
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";
#else
const char service_interp[] __attribute__((section(".interp"))) =
"/lib/ld-linux.so.2";
#endif
- 构建了没有优化的库(带有
-O0编译器标志)。
我们制作了一个简单的演示项目来说明这一思想。为了证明动态库的真正双重性(即,即使它现在可以作为可执行文件运行,它仍然能够作为常规动态库运行),演示项目不仅包含演示动态库,还包含动态加载它并调用其printMessage()函数的可执行文件。清单 11-1 说明了可执行共享库项目的细节:
清单 11-1。
文件:executableSharedLib.c
#include "sharedLibExports.h"
#include <unistd.h> // needed for the _exit() function
// Must define the interpretor to be the dynamic linker
#ifdef __LP64__
const char service_interp[] __attribute__((section(".interp"))) =
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2";
#else
const char service_interp[] __attribute__((section(".interp"))) =
"/lib/ld-linux.so.2";
#endif
void printMessage(void)
{
printf("Running the function exported from the shared library\n");
}
int main(int argc, char* argv[])
{
printf("Shared library %s() function\n", __FUNCTION__);
// must make the entry point function to be a 'no-return' function type
_exit(0);
}
文件:build.sh
g++ -Wall -O0 -fPIC -I./exports/ -c src/executableSharedLib.c -o src/executableSharedLib.o
g++ -shared``-Wl,-e,main
清单 11-2 展示了演示应用程序的细节,其目的是证明我们的共享库在变得可执行的同时并没有失去它原来的功能:
清单 11-2。
文件:main.c
#include <stdio.h>
#include "sharedLibExports.h"
int main(int argc, char* argv[])
{
printMessage();
return 0;
}
文件:build.sh
g++ -Wall -O2 -I../sharedLib/exports/ -c src/main.c -o src/main.o
g++ ./src/main.o -lpthread -lm -ldl -L../deploy -lexecutablesharedlib -Wl,-Bdynamic -Wl,-R../deploy -o demoApp
当您尝试使用它时,会出现图 11-3 所示的结果。
图 11-3。
Illustrating dual nature (dynamic lib, executable) of the demo library
项目源代码 tarball 提供了更多的细节。
Windows 库的运行时内存处理冲突
一般来说,一旦动态库被加载到进程中,它就成为进程的一个合法部分,并且几乎继承了进程的所有特权,包括对堆(运行动态内存分配的内存池)的访问。由于这些原因,动态库函数分配内存缓冲区,并将其传递给属于另一个动态库(或可执行代码)的函数是完全正常的,当不再需要内存时,可以在那里释放内存。
然而,整个故事有一个特殊的转折,需要仔细研究。
通常,不管有多少动态库被加载到进程中,它们都链接到 C runtime library 的同一个实例,该实例提供内存分配基础结构 malloc 和 free(或者对于 C++,new 和 delete ),以及跟踪分配的内存缓冲区的列表实现。如果这个基础设施对每个进程都是唯一的,那么我们就没有理由认为所描述的任何人都可以释放由其他人分配的内存的方案不可行。
然而,有趣的情况可能发生在 Windows 编程领域。Visual Studio 提供了(至少)两个基本 dll,所有可执行文件(应用程序/动态库)都是在这两个 dll 的基础上构建的——常见的 C 运行时库(msvcrt.dll)以及微软基础类(MFC)库(mfx42.dll)。有时,项目需求可能会要求混合和匹配构建在不同基 dll 上的 dll,这可能会立即导致与预期规则非常不愉快的偏差。
为了清楚起见,我们假设在同一个项目中,您在运行时加载了以下两个 DLL:DLL“A”,构建在msvcrt.dll上,以及 DLL“B”,构建在MFC DLL上。现在让我们假设 DLL“A”分配内存缓冲区并将其传递给 DLL“B”,DLL“B”使用它们,然后释放它们。在这种情况下,试图释放内存将导致崩溃(异常如图 11-4 所示)。
图 11-4。
Error message dialog typical for between-DLLs-conflict memory issues
问题的原因是围绕堆内存的可用池有两个簿记机构;C 运行时 DLL 和 MFC DLL 都维护它们自己的、单独的已分配缓冲区列表(参见图 11-5 )。
图 11-5。
The mechanism of runtime problems caused by unrelated memory allocation bookkeepings maintained by different DLLs
通常,当发送缓冲区进行解除分配时,内存分配基础结构会搜索已分配内存地址的列表,如果在列表中找到了为解除分配而传递的缓冲区,则可以成功完成解除分配。但是,如果分配的缓冲区保存在一个列表中(例如,由 C 运行库 DLL 保存)并被传递给另一个列表(例如,由 MFC DLL 保存),则在列表中找不到缓冲区的内存地址,并且解除分配调用将引发异常。即使您以静默方式处理异常,应用程序是否能够将缓冲区发送到正确的 DLL 进行释放也是有疑问的,从而导致内存泄漏。
更糟糕的是,几乎没有一个通常的内存受限的检查工具能够检测和报告任何错误。在为工具辩护时,您可以注意到,在这种特殊情况下,实际上没有发生任何典型的内存违规(比如写超过缓冲区边界、覆盖缓冲区地址等)。所有这些都使得问题难以处理,除非您事先对潜在的问题有所了解,否则很难确定原因,更不用说问题的解决方案了。
这个问题的解决方案非常简单:在一个 DLL 中分配的内存缓冲区最终应该传递回同一个 DLL 进行释放。唯一的问题是,为了应用这个简单的解决方案,您需要访问两个 dll 的源代码,这可能并不总是可行的。
链接器弱符号解释
链接器弱符号的思想本质上类似于面向对象语言的首要特征(这是多态原理的表现之一)。当应用于链接领域时,弱符号的概念实际上意味着以下内容:
- 编译器(最明显的是,
gcc)支持语言构造,允许你声明一个符号(一个函数和/或一个全局或函数静态变量)为弱。
下面的示例演示如何将 C 函数声明为弱符号:
int``__attribute__((weak))
- 链接器获取这些信息,以一种非常独特的方式处理这样的符号。
- 如果在链接过程中出现了另一个同名符号,并且没有声明为弱符号,则该符号将替换弱符号。
- 如果在链接过程中出现了另一个同名的符号,并且被声明为弱符号,链接器可以自由决定实际实现这两个符号中的哪一个。
- 两个同名的非弱(即强)符号的出现被认为是错误的(该符号已经定义)。
- 如果在链接过程中没有出现其他同名的符号,链接器可能不会实现这样的符号。如果符号是一个函数指针,保护代码是必须的(事实上,强烈建议总是这样做)。
在 Winfred C.H. Lu 在 http://winfred-lu.blogspot.com/2009/11/understand-weak-symbols-by-examples.html 发表的博客文章中可以找到弱符号概念的一个很好的说明。安迪·穆雷在 www.embedded-bits.co.uk/2008/gcc-weak-symbols/ 的博客中描述了这些功能何时会派上用场的真实场景。
十二、Linux 工具箱
Abstract
本章的目的是向读者介绍一套用于分析 Linux 二进制文件内容的工具(实用程序和其他方法)。
本章的目的是向读者介绍一套用于分析 Linux 二进制文件内容的工具(实用程序和其他方法)。
快速洞察工具
使用file和/或size实用程序可以最简单、最直接地了解二进制文件的本质。
文件实用程序
简单地命名为file ( http://linux.die.net/man/1/file )的命令行实用程序用于查找任何文件类型的详细信息。它可以很快派上用场,因为它确定了关于二进制文件的最基本的信息(图 12-1 )。
图 12-1。
Using the file utility
尺寸实用程序
名为size ( http://linux.die.net/man/1/size )的命令行实用程序可用于即时了解 ELF 部分的字节长度(图 12-2 )。
图 12-2。
Using the size utility
详细的分析工具
通过依赖于统称为binutils ( www.gnu.org/software/binutils/)的实用程序集合,可以获得对二进制文件属性的详细了解。我将举例说明ldd、nm、objdump和readelf实用程序的使用。尽管它在形式上不属于 binutils,但名为 ldd 的 shell 脚本(由 Roland McGrath 和 Ulrich Drepper 编写)非常适合放在工具箱的同一个隔间中,因此也将说明它的用法。
掺杂漏极
命令ldd ( http://linux.die.net/man/1/ldd )是一个非常有用的工具,因为它显示了动态库的完整列表,客户端二进制程序将尝试静态感知这些动态库的负载(即,负载时间依赖性)。
当分析加载时依赖关系时,ldd首先检查二进制文件,试图定位 ELF 格式字段,其中最直接的依赖关系列表已经被链接器打上印记(如构建过程中链接器命令行所建议的)。
对于每个名字嵌入在客户端二进制文件中的动态库,ldd试图根据运行时库位置搜索规则定位它们实际的二进制文件(详见第七章中的)。一旦定位了最直接的依赖关系的二进制文件,ldd运行递归过程的下一级,试图找到它们的依赖关系。对每一个“第二代”依赖者,ldd进行另一轮调查,等等。
一旦所述的递归搜索完成,ldd收集报告的依赖项列表,删除重复项,并打印出结果(如图 12-3 )。
图 12-3。
Using the ldd utility
在使用ldd之前,了解它的局限性很重要:
ldd无法通过调用dlopen()函数来识别运行时动态加载的库。为了获得这类信息,必须采用不同的方法。更多详情请访问第十三章。- 根据其手册页,运行某些
ldd版本实际上可能代表一种安全威胁。
更安全的 ldd 替代品
如手册页中所述:
但是,请注意,在某些情况下,某些版本的 ldd 可能试图通过直接执行程序来获取依赖信息。因此,您不应该在不可信的可执行文件上使用 ldd,因为这可能导致任意代码的执行。在处理不受信任的可执行文件时,一个更安全的替代方法如下(也如图 12-4 所示):
$ objdump -p /path/to/program | grep NEEDED
图 12-4。
Using objdump to (only partially) substitute the ldd utility
使用readelf实用程序可以获得相同的结果(图 12-5 ):
$ readelf -d /path/to/program | grep NEEDED
图 12-5。
Using readelf to (only partially) substitute the ldd utility
显然,在依赖关系的分析中,这两种工具都不会比仅仅从二进制文件中读出最直接的依赖关系列表更深入。从安全的角度来看,这绝对是一种更安全的寻找答案的方法。
然而,所提供的列表远没有ldd通常所提供的那样详尽。为了匹配它,您可能需要自己进行递归搜索。
纳米
nm实用程序( http://linux.die.net/man/1/nm )用于列出一个二进制文件的符号(图 12-6 )。打印出符号的输出行还指示了符号类型。如果二进制文件包含 C++ 代码,则默认情况下会以损坏的形式打印符号。以下是一些最常用的输入参数组合:
图 12-7。
Using the nm utility to list mangled symbols
$ nm -D --no-demangle <path-to-binary>打印共享库的动态符号,并严格要求符号不混乱(图 12-7 )。
图 12-6。
Using the nm utility to list unmangled symbols
$ nm <path-to-binary>列出一个二进制文件的所有符号。在共享库的情况下,它不仅意味着导出(的.dynamic部分),还意味着所有其他符号。如果库已经被剥离(通过使用strip命令),没有参数的nm将报告没有找到符号。$ nm -D <path-to-binary>仅列出动态部分中的符号(即共享库的导出/可见符号)。$ nm -C <path-to-binary以分解的格式列出符号(图 12-6 )。
该选项对于检测设计共享库时最常见的错误非常有用——当设计者忘记了 ABI 函数声明/定义中的extern“C”说明符时(这恰好是客户端二进制文件期望找到的)。
-
当您想要列出库的未定义符号时,
$ nm -u <path-to-binary>是有用的(即,库本身不包含的符号,但指望在运行时提供,可能由一些其他加载的动态库提供)。 -
$ nm -A <library-folder-path>/* | grep symbol-nameis useful when you search for a symbol in multitude of binaries located in the same folder, as-Aoption prints the name of each library in which a symbols is found (Figure 12-8) .图 12-8。
Using nm to recursively search for the presence of a symbol in the set of libraries .
位于 www.thegeekstuff.com/2012/03/linux-nm-command/ 的网页文章列出了 10 个最有用的nm命令。
objdump(对象转储)
objdump ( http://linux.die.net/man/1/objdump )实用程序可能是最通用的二元分析工具。按时间顺序,它比readelf更古老,这在很多情况下与其能力相当。objdump的优势在于除了 ELF,还支持大约 50 种其他二进制格式。而且,它的拆卸能力比readelf更好。
以下章节涵盖了最常使用objdump的任务。
解析 ELF 标头
objdump -f命令行选项用于深入了解目标文件的文件头。标题提供了大量有用的信息。特别是,可以快速获得二进制类型(目标文件/静态库对动态库对可执行文件)以及关于入口点的信息(?? 段的开始)(图 12-9 )。
图 12-9。
Using objdump to parse the ELF header of various binary file types
当检查静态库时,objdump -f打印出在库中找到的每个目标文件的文件头。
列出和检查部分
objdump -h选项用于列出可用的截面(图 12-10 )。
图 12-10。
Using objdump to list the binary file sections
当涉及到部分检查时,objdump为程序员最感兴趣的部分提供了专用的命令开关。在接下来的几节中,我将介绍一些著名的例子。
列出所有符号
运行objdump -t <path-to-binary>提供完全等同于运行nm <path-to-binary>的输出(图 12-11 )。
图 12-11。
Using objdump to list all symbols
仅列出动态符号
运行objdump -T <path-to-binary>提供完全等同于运行nm -D <path-to-binary>的输出(图 12-12 )。
图 12-12。
Using objdump to list only dynamic symbols
检查动态部分
运行objdump -p <path-to-binary>检查动态部分(用于查找DT_RPATH和/或DT_RUNPATH设置)。请注意,在这种情况下,您关心显示输出的最后部分(图 12-13 )。
图 12-13。
Using objdump to examine the library dynamic section
检查重新安置部分
运行objdump -R <path-to-binary>检查搬迁段(图 12-14 )。
图 12-14。
Using objdump to list the relocation section
检查数据部分
运行objdump -s -j <section name> <path-to-binary>提供截面所携带值的十六进制转储。在图 12-15 中为.got段。
图 12-15。
Using objdump to examine the data section
列出和检查细分市场
运行objdump -p <path-to-binary>显示关于 ELF 二进制段的信息。请注意,只有显示输出的第一部分与该特定任务相关(图 12-16 )。
图 12-16。
Using objdump to list segments
反汇编代码
下面是一些如何使用objdump反汇编代码的例子:
-
Disassembling and Intel style and interspersing the original source code (Figure 12-18) .
图 12-18。
Using objdump to disassemble the binary file (Intel syntax) .
-
Disassembling and specifying assembler notation flavor (Intel style in this case), as shown in Figure 12-17 .
图 12-17。
Using objdump to disassemble the binary file .
仅当二进制文件是为调试而构建时,该选项才有效(即,使用-g选项)。
- 分解特定部分。
除了携带代码的.text部分,二进制文件可能包含其他部分(。plt为例),其中也包含代码。默认情况下,objdump反汇编所有带有代码的部分。然而,在某些情况下,您可能对检查某个给定部分严格执行的代码感兴趣(图 12-19 )。
图 12-19。
Using objdump to disassemble a specific section
objdump nm 当量
objdump可用于提供nm命令的完全等效:
$ nm <path-to-binary>
等同于
$ objdump -t <path-to-binary>
$ nm -D <path-to-binary>
等同于
$ objdump -T <path-to-binary>
$ nm -C <path-to-binary>
等同于
$ objdump -C <path-to-binary>
readelf(读取 11)
readelf ( http://linux.die.net/man/1/readelf )命令行实用程序提供了与objdump实用程序几乎完全相同的功能。readelf 和 objdump 之间最显著的区别是
readelf仅支持 ELF 二进制格式。另一方面,objdump可以分析大约 50 种不同的二进制格式,包括 Windows PE/COFF 格式。readelf不依赖于所有 GNU 目标文件解析工具所依赖的二进制文件描述符库(http://en.wikipedia.org/wiki/Binary_File_Descriptor_library),从而提供了对 ELF 格式内容的独立洞察
接下来的两节提供了使用objdump的最常见任务的概述。
解析 ELF 标头
readelf -h命令行选项用于深入了解目标文件的文件头。标题提供了大量有用的信息。特别是,可以快速获得二进制类型(目标文件/静态库对动态库对可执行文件)以及关于入口点的信息(?? 段的开始)(图 12-20 )。
图 12-20。
Examples of using readelf to examine the ELF header of executable, shared library, and object file/static library
当检查静态库时,readelf -h打印出在库中找到的每个目标文件的文件头。
列出和检查部分
readelf -S选项用于列出可用的截面(图 12-21 )。
图 12-21。
Using readelf to list sections
当涉及到部分检查时,readelf为程序员最感兴趣的部分提供了专用的命令开关,例如.symtab、.dynsym和.dynamic部分。
列出所有符号
运行readelf --symbols提供完全等同于运行nm <path-to-binary>的输出(图 12-22 )。
图 12-22。
Using readelf to list all symbols
仅列出动态符号
运行readelf --dyn-syms提供完全等同于运行nm -D <path-to-binary>的输出(图 12-23 )。
图 12-23。
Using readelf to list dynamic symbols
检查动态部分
运行readelf -d检查动态部分(用于查找DT_RPATH和/或DT_RUNPATH设置),如图 12-24 所示。
图 12-24。
Using readelf to display the dynamic section
检查搬迁部分
运行readelf -r检查搬迁段,如图 12-25 所示。
图 12-25。
Using readelf to list relocation (.rel.dyn) section
检查数据部分
运行readelf -x提供截面所携带值的十六进制转储。在图 12-26 中为.got段。
图 12-26。
Using readelf to provide a hex dump of a section (the .got section in this example)
列出和检查细分市场
运行readelf --segments显示关于 ELF 二进制段的信息(图 12-27 )。
图 12-27。
Using readelf to examine segments
检测调试版本
readelf命令很好地支持显示二进制文件中包含的各种调试特定信息(图 12-28 )。
图 12-28。
Readelf provides the option to examine binary file debug information
为了快速确定二进制文件是否是为调试而构建的,在调试构建的情况下,使用任何可用选项运行readelf --debug-dump的输出将由打印在stdout上的许多行组成。相反,如果二进制文件不是为调试而构建的,输出将是一个空行。在二进制文件包含调试信息的情况下,限制输出喷涌的一个快速而实用的方法是将 readelf 输出通过管道传输到wc命令:
$``readelf --debug-dump=line``<binary file path>
或者,可以使用下面的简单脚本以简单明了的文本形式显示readelf的调查结果。它要求将二进制文件的路径作为输入参数传递。
file: isDebugVersion.sh
if``readelf --debug-dump=line
部署阶段工具
在成功构建二进制文件并开始考虑部署阶段的细节后,诸如chrpath、patchelf、strip和ldconfig等实用程序可能会派上用场。
chrpath
chrpath命令行实用程序( http://linux.die.net/man/1/chrpath )用于修改 ELF 二进制文件的rpath ( DT_RPATH字段)。在“Linux 运行时库位置规则”一节的第七章中描述了runpath字段背后的基本概念。
以下细节说明了chrpath的使用(图 12-29 )以及一些限制(图 12-30 ):
- 它可用于在其原始字符串长度内修改
DT_RPATH。 - 它可以用来删除现有的
DT_RPATH字段。
但是,一定要谨慎!
图 12-30。
Limitations of the chrpath utility
图 12-29。
Using the chrpath utility to modify RPATH
- 如果
DT_RPATH字符串最初为空,则不能用新的非空字符串替换。 - 可用于将
DT_RPATH转换为DT_RUNPATH。 - 它不能用更长的字符串替换现有的
DT_RPATH字符串。
补丁精灵
有用的patchelf ( http://nixos.org/patchelf.html )命令行实用程序目前不是标准存储库的一部分,但是可以从源代码 tarball 构建它。简单、基本的文档也是可用的。
该实用程序可用于设置和修改 ELF 二进制文件的runpath ( DT_RUNPATH字段)。在第七章的“Linux 运行时库位置规则”一节中描述了runpath字段背后的基本概念。
设置runpath最简单的方法是发出如下命令:
$``patchelf --set-rpath
^
|
multiple paths can be defined,
separated by a colon (:)
patchelf修改DT_RUNPATH字段的能力远远超过了chrpath修改DT_RPATH字段的能力,因为它可以以任何想象得到的方式修改DT_RUNPATH的字符串值(替换为更短或更长的字符串、插入多条路径、擦除等)。).
剥夺
strip命令行实用程序( http://linux.die.net/man/1/strip )可用于删除动态加载过程中不需要的所有库符号。在第七章的“导出 Linux 动态库符号”一节中演示了条带效果。
ldconfig
在第七章(专门讨论 Linux 运行时库位置规则)中,我指出了指定加载程序在运行时应该寻找库的路径的方法之一(尽管不是最高优先级)是通过使用ldconfig缓存。
ldconfig命令行实用程序( http://linux.die.net/man/8/ldconfig )通常作为软件包安装过程的最后一步执行。当包含共享库的路径作为输入参数传递给ldconfig时,它搜索共享库的路径,并更新它用于簿记的文件集:
- 包含标准扫描的文件夹列表的文件
/etc/ld.so.conf - 文件
/etc/ld.so.cache文件,包含在扫描作为输入参数传递的各种路径时发现的所有库的 ASCII 文本列表
运行时分析工具
通过使用诸如strace、addr2line,尤其是 GNU 调试器(gdb)这样的工具,运行时问题的分析可能会受益。
失去了
strace ( http://linux.die.net/man/1/strace )命令行实用程序跟踪进程发出的系统调用以及进程收到的信号。它有助于找出运行时依赖关系(即,不仅仅是ldd命令适用的加载时依赖关系)。图 12-31 显示了典型的 strace 输出。
图 12-31。
Using the strace utility
addr2line
可以使用addr2line ( http://linux.die.net/man/1/addr2line )命令行实用程序将运行时地址转换成源文件的信息和地址对应的行号。
如果(且仅当)二进制文件是为调试而构建的(通过传递-g -O0编译器标志),当分析崩溃信息时,使用该命令可能非常有用,其中崩溃发生的程序计数器地址打印在终端屏幕上,如下所示:
#00 pc 0000d8cc6 /usr/mylibs/libxyz.so
在这样的控制台输出上运行addr2line
$ addr2line``-C -f -e
将产生如下所示的输出:
/projects/mylib/src/mylib.c: 45
gdb (GNU 调试器)
传说中的 GNU 调试器工具 gdb 可以用来执行运行时代码反汇编。在运行时反汇编代码的好处是所有的地址都已经被加载器解析过了,而且大部分地址都是最终的。
以下gdb命令在运行时代码反汇编期间会很有用:
- 设置反汇编风味
- 拆卸
调用反汇编命令时,以下两个标志可能会派上用场:
图 12-33。
Interspersed (assembly and source code) disassembly flavor
-
/m标志将汇编指令散布在 C/C++ 代码行中(如果有的话),如图 12-33 所示。 -
The
/rflag requires that the assembler instructions be additionally shown in hexadecimal notation (Figure 12-32) .图 12-32。
Using gdb to show the disassembled code combined with hex values of instructions .
要组合这两个标志,请将它们组合在一起键入(即/rm),而不是分开键入(即/r /m),如图 12-34 所示。
图 12-34。
Combining /r and /m disassembly flags
静态库工具
与静态库相关的绝大多数任务都可以由归档器ar实用程序来执行。通过使用ar,您不仅可以将目标文件合并到静态库中,还可以列出其内容,删除单个目标文件,或者用新版本替换它们。
阿肯色州
以下简单的例子说明了使用ar工具的通常阶段。演示项目由四个源文件(first.c、second.c、third.c和fourth.c)和一个可以被客户端二进制文件使用的导出头文件组成(如下面五个例子所示)。
first.c
#include "mystaticlibexports.h"
int first_function(int x)
{
return (x+1);
}
秒. c
#include "mystaticlibexports.h"
int fourth_function(int x)
{
return (x+4);
}
三. c
#include "mystaticlibexports.h"
int second_function(int x)
{
return (x+2);
}
第四. c
#include "mystaticlibexports.h"
int third_function(int x)
{
return (x+3);
}
mystaticlibexports . h .神秘主义者的出口
#pragma once
int first_function(int x);
int second_function(int x);
int third_function(int x);
int fourth_function(int x);
假设您已经通过编译每个源文件创建了目标文件:
$ gcc -Wall -c first.c second.c third.c fourth.c
下面的屏幕快照说明了处理静态库的各个阶段。
创建静态库
运行ar -rcs <library name> <list of object files>将指定的目标文件合并到静态库中(图 12-35 )。
图 12-35。
Using ar to combine object files to static library
列出静态库对象文件
运行ar -t <library name>打印出静态库携带的目标文件列表(图 12-36 )。
图 12-36。
Using ar to print out the list of static library’s object files
从静态库中删除目标文件
假设您想要修改文件first.c(修复一个 bug,或者简单地添加额外的特性),并且暂时不希望您的静态库携带first.o object file。从静态库中删除目标文件的方法是运行ar -d <library name> <object file to remove>(图 12-37 )。
图 12-37。
Using ar to delete an object file from static library
将新的目标文件添加到静态库中
假设您对文件first.c中所做的更改感到满意,并且已经重新编译了它。现在您想把新创建的目标文件first.o放回静态库中。运行ar -r <library name> <object file to append>基本上是将新的目标文件添加到静态库中(图 12-38 )。
图 12-38。
Using ar to add new object file to static library
请注意,目标文件在静态库中的顺序已经改变。新文件已被有效地添加到档案中。
恢复目标文件的顺序
如果您坚持让您的目标文件以代码更改前的原始顺序出现,您可以纠正它。运行ar -m -b <object file before> <library name> <object file to move>完成任务(图 12-39 )。
图 12-39。
Using ar to restore the order of object files within the static library
十三、Linux 操作指南
Abstract
前一章回顾了 Linux 中可用的有用的分析工具,所以现在是提供同一主题的另一种观点的好时机。这一次的重点不是实用程序本身,而是展示如何完成一些最常执行的任务。
前一章回顾了 Linux 中可用的有用的分析工具,所以现在是提供同一主题的另一种观点的好时机。这一次的重点不是实用程序本身,而是展示如何完成一些最常执行的任务。
通常有多种方法来完成分析任务。对于本章中描述的每项任务,将提供完成任务的替代方法。
调试链接
调试链接阶段最有力的帮助可能是使用LD_DEBUG环境变量(图 13-1 )。它不仅适用于测试构建过程,也适用于测试运行时的动态库加载。
操作系统支持一组预先确定的值,在运行所需的操作(构建或执行)之前,可以将LD_DEBUG设置为这些值。列出它们的方法是键入
$ LD_DEBUG=help cat
图 13-1。
Using the LD_DEBUG environment variable to debug linking
与任何其他环境变量一样,有几种方法可以设置LD_DEBUG的值:
- 立即,在调用链接器的同一行
- 终端外壳寿命期内一次
$ export LD_DEBUG=<chosen_option>
这可以通过以下方式逆转
$ unset LD_DEBUG
- 从外壳轮廓内(例如
bashrc)文件,为每个终端会话设置它。除非您的日常工作是测试链接过程,否则这个选项可能不是最佳选项。
确定二进制文件类型
有几种简单的方法可以确定二进制类型:
file实用程序(在它能处理的各种各样的文件类型中)提供了可能是最简单、最快和最优雅的方法来确定二进制文件的性质。- ELF 文件头分析提供了关于二进制文件类型的信息。运转
$ readelf -h <path-of-binary> | grep Type
将显示以下选项之一:
EXEC(可执行文件)DYN(共享对象文件)REL(可重定位文件)
在静态库的情况下,REL输出将为库携带的每个目标文件出现一次。
- EFL 标题分析可提供类似的分析,但报告不太详细。该命令的输出
$ objdump -f <path-of-binary>
将有一条包含下列值之一的线:
EXEC_P(可执行文件)DYNAMIC(共享对象文件)- 在目标文件的情况下,没有指明类型
在静态库的情况下,一个目标文件将为库携带的每个目标文件出现一次。
确定二进制文件入口点
确定二进制文件入口点是一项复杂的任务,从非常简单(对于可执行文件)到稍微复杂一些(在运行时确定动态库的入口点),这两者都将在本节中进行说明。
确定可执行文件入口点
可执行文件的入口点(即程序存储器映射中第一条指令的地址)可以通过以下任一方法确定
- ELF 文件头分析,它提供了二进制文件类型的详细信息。运转
$ readelf -h <path-of-binary> | grep Entry
将显示如下所示的一行:
Entry point address: 0x<address>
objdumpEFL 标题分析,可提供类似分析,但不太详细的报告。该命令的输出
$ objdump -f <path-of-binary> | grep start
看起来会像这样:
start address 0x<address>
确定动态库入口点
当寻找动态库的入口点时,调查并不简单。即使可以使用前面描述的方法之一,所提供的信息(通常是低值的十六进制数,如 0x390)也不是特别有用。假设动态库被映射到客户端二进制进程存储器映射中,则库的真正入口点可能仅在运行时被确定。
最简单的方法可能是在 gnu 调试器中运行加载动态库的可执行文件。如果设置了LD_DEBUG环境变量,将会打印出关于加载的库的信息。您需要做的就是在main()函数上设置断点。无论可执行文件是否是为调试而生成的,此符号都很可能存在。
在动态库以静态方式链接的情况下,当程序执行到达断点时,加载过程已经完成。
在运行时动态加载的情况下,最简单的方法可能是将大量的屏幕打印输出重定向到文件,以便以后进行可视化检查。
图 13-2 展示了依赖于LD_DEBUG变量的方法。
图 13-2。
Determining the dynamic library entry point at runtimeList Symbols
列出符号
尝试列出可执行文件和库的符号时,可以遵循以下方法:
nm效用readelf效用
特别是,
- 运行以下命令可以获得所有可见符号的列表
$ readelf --symbols <path-to-binary>
- 可以通过运行以下命令来获得一个列表,其中只列出了出于动态链接目的而导出的符号
$ readelf --dyn-syms <path-to-binary>
objdump效用
特别是,
- 运行以下命令可以获得所有可见符号的列表
$ objdump -t <path-to-binary>
- 可以通过运行以下命令来获得一个列表,其中只列出了出于动态链接目的而导出的符号
$ objdump -T <path-to-binary>
列出并检查部分
有几种方法可以获得关于二进制部分的信息。运行size命令可以获得最快速和最基本的洞察。对于更结构化和更详细的洞察,您通常可以依赖像objdump和/或readelf这样的工具,后者是严格按照 ELF 二进制格式专门化的。通常,强制性的第一步是列出二进制文件中存在的所有部分。一旦获得这样的洞察力,就详细检查特定片段的内容。
列出可用的部分
ELF 二进制文件的节列表可以通过以下方法之一获得:
readelf效用
$ readelf -S <path-to-binary>
objdump效用
$ objdump -t <path-to-binary>
检查特定部分
到目前为止,最常检查的部分是包含链接器符号的部分。因此,已经开发了各种各样的工具来满足这一特定需求。出于同样的原因,尽管描述符号提取的段落属于检查各部分的大类,但是它已经作为单独的主题被首先提出。
检查动态部分
二进制文件的动态部分(特别是动态库)包含大量有趣的信息。列出此特定部分的内容可以通过以下方式之一来完成:
readelf效用
$ readelf -d <path-to-binary>
objdump效用
$ objdump -p <path-to-binary>
在可从动态部分提取的有用信息中,以下是极有价值的信息:
DT_RPATH或DT_RUNPATH字段的值- 动态库
SONAME字段的值 - 所需动态库的列表(
DT_NEEDED字段)
确定动态库是 PIC 还是 LTR
如果动态库是在没有-fPIC编译器标志的情况下构建的,那么它的动态部分将包含TEXTREL字段,否则该字段将不会出现。以下简单的脚本(pic_or_ltr.sh)可以帮助您确定动态库是否是用-fPIC标志构建的:
if readelf -d $1 | grep TEXTREL > /dev/null; \
then echo "library is LTR, built without the -fPIC flag"; \
else echo "library was built with -fPIC flag"; fi
检查搬迁部分
这项任务可以通过以下方式完成:
readelf效用
$ readelf -r <path-to-binary>
objdump效用
$ objdump -R <path-to-binary>
检查数据部分
这项任务可以通过以下方式完成:
readelf效用
$ readelf -x <section name> <path-to-binary>
objdump效用
$ objdump -s -j <section name> <path-to-binary>
列出并检查细分市场
这项任务可以通过以下方式完成:
readelf效用
$ readelf --segments <path-to-binary>
objdump效用
$ objdump -p <path-to-binary>
反汇编代码
在本节中,您将研究反汇编代码的不同方法。
反汇编二进制文件
这个特殊任务的最佳工具是objdump命令。事实上,这可能是唯一一种readelf不提供并行解决方案的情况。特别是,.text部分可以通过运行来拆卸
$ objdump``-d
此外,您可以指定打印输出的风格(美国电话电报公司与英特尔)。
$ objdump -d``-M intel
如果您想查看散布在汇编指令中的源代码(如果有的话),您可以运行以下命令:
$ objdump -d -M intel``-S
最后,您可能希望分析给定部分中的代码。除了。以携带代码而臭名昭著的 section,其他一些 section(。例如plt)可以包含源代码。
默认情况下,objdump反汇编所有代码段。要指定要拆卸的单个部件,使用-j选项:
$ objdump -d -S -M intel``-j .plt
分解正在运行的进程
最好的方法是依靠 gdb 调试器。请参考前一章专门介绍这个奇妙工具的部分。
标识调试版本
看起来,识别二进制文件是否是为调试而构建的(即,使用-g选项)的最可靠的方法是依靠readelf工具。尤其是跑步
$ readelf --debug-dump=line <path-to-binary>
在二进制文件的调试版本的情况下,将提供非空输出。
列出加载时相关性
要列出可执行文件(应用程序和/或共享库)在加载时所依赖的共享库集合,请仔细阅读关于ldd命令的讨论(其中提到了ldd方法和基于objdump的更安全的方法)。
简而言之,运行 ldd
$ ldd <path-to-binary>
将提供依赖项的完整列表。
或者,依靠objdump或readelf来检查二进制文件的动态部分是一个更安全的提议,其代价是只提供第一级依赖关系。
$ objdump -p /path/to/program | grep NEEDED
$ readelf -d /path/to/program | grep NEEDED
列出加载程序已知的库
要列出所有运行时路径已知且对加载器可用的库,您可以依赖于ldconfig实用程序。运转
$ ldconfig -p
将打印加载程序已知的库的完整列表(即当前存在于/etc/ld.so.cache文件中)及其各自的路径。
因此,在加载程序可用的整个库列表中搜索特定的库可以通过运行
$ ldconfig -p | grep <library-of-interest>
列出动态链接库
与本章到目前为止列出的任务相反,这个特定的任务在二进制分析工具的上下文中没有被提及。原因很简单:当运行时动态库加载发生时,二进制文件分析工具在运行时用处不大。像ldd这样的工具不包含运行时通过调用dlopen()函数加载的动态库。
以下方法将提供加载的动态库的完整列表。该列表包括静态感知时动态链接的库以及运行时动态链接的库。
strace 实用程序
调用strace <program command line>是一种列出系统调用序列的有用方法,其中open()和mmap()是我们最感兴趣的。该方法显示加载的共享库的完整列表。每当提到共享库时,通常在mmap()调用下面的几行输出会显示加载地址。
LD_DEBUG 环境变量
鉴于它的灵活性和广泛的选择,这个选项总是出现在跟踪与链接/加载过程相关的一切的工具列表中。对于这个特殊的问题,LD_DEBUG=files选项可能会提供大量的打印输出,携带运行时动态加载的库的过多信息(它们的名称、运行时路径、入口点地址等)。).
/proc/ /maps 文件
每当一个进程运行时,Linux 操作系统在/proc文件夹下维护一组文件,跟踪与该进程相关的重要细节。特别是,对于 PID 为 NNNN 的进程,位置/proc/<NNNN>/maps的文件包含库列表和它们各自的加载地址。例如,图 13-3 显示了这个方法为 Firefox 浏览器报告的内容。
图 13-3。
Examining /proc//maps file to examine process memory map
备注 1:
一个潜在的小问题可能是某些应用程序完成得很快,没有留下足够的时间来检查进程内存映射。在这种情况下,最简单快捷的解决方案是通过 gdb 调试器启动进程,并在主函数上设置一个断点。当程序执行在断点处保持阻塞时,您将有无限的时间来检查进程内存映射。
备注 2:
如果您确定当前只有一个程序实例正在执行,那么您可以依靠pgrep (process grep)命令来消除查找进程 PID 的需要。对于 Firefox 浏览器,您应该键入
$ cat /proc/pgrep firefox/maps
lsof 实用程序
lsof实用程序分析正在运行的进程,并在标准输出流中打印出进程打开的所有文件的列表。如其手册页( http://linux.die.net/man/8/lsof )所述,打开的文件可以是常规文件、目录、块专用文件、字符专用文件、执行文本引用、库、流或网络文件(互联网套接字、NFS 文件或 UNIX 域套接字)。
在它报告打开的文件类型的广泛选择中,它还报告由进程加载的动态库的列表,不管加载是静态感知的还是动态执行的(通过在运行时运行dlopen)。
下面的截图展示了如何获取 Firefox 浏览器打开的所有共享库的列表,如图 13-4 所示:
$``lsof -p
图 13-4。
Using the lsof utility to examine process memory map
注意lsof提供了定期运行过程检查的命令行选项。通过指定检查周期,您可以捕捉到运行时动态加载和卸载发生的时刻。
使用-r选项运行lsof时,周期性过程检查会无限循环下去,要求用户按 Ctrl-C 终止。用+r选项运行lsof具有当不再检测到打开的文件时lsof终止的效果。
程序化方式
也可以编写代码,打印出进程正在加载的库。当应用程序代码包含对dl_iterate_phdr()函数的调用时,它在运行时的打印输出可以帮助您确定它加载的共享库的完整列表,以及与每个库相关联的额外数据(例如加载的库起始地址)。
为了说明这个概念,我们创建了由一个驱动程序和两个简单的动态库组成的演示代码。应用程序的源文件显示在以下示例中。其中一个动态库是静态感知的动态链接,而另一个库是通过调用dlopen()函数动态加载的:
#define _GNU_SOURCE
#include <link.h>
#include <stdio.h>
#include <dlfcn.h>
#include "sharedLib1Functions.h"
#include "sharedLib2Functions.h"
static const char* segment_type_to_string(uint32_t type)
{
switch(type)
{
case PT_NULL: // 0
return "Unused";
break;
case PT_LOAD: // 1
return "Loadable Program Segment";
break;
case PT_DYNAMIC: //2
return "Dynamic linking information";
break;
case PT_INTERP: // 3
return "Program interpreter";
break;
case PT_NOTE: // 4
return "Auxiliary information";
break;
case PT_SHLIB: // 5
return "Reserved";
break;
case PT_PHDR: // 6
return "Entry for header table itself";
break;
case PT_TLS: // 7
return "Thread-local storage segment";
break;
// case PT_NUM: // 8 /* Number of defined types */
case PT_LOOS: // 0x60000000
return "Start of OS-specific";
break;
case PT_GNU_EH_FRAME: // 0x6474e550
return "GCC .eh_frame_hdr segment";
break;
case PT_GNU_STACK: // 0x6474e551
return "Indicates stack executability";
break;
case PT_GNU_RELRO: // 0x6474e552
return "Read-only after relocation";
break;
// case PT_LOSUNW: // 0x6ffffffa
case PT_SUNWBSS: // 0x6ffffffa
return "Sun Specific segment";
break;
case PT_SUNWSTACK: // 0x6ffffffb
return "Sun Stack segment";
break;
// case PT_HISUNW: // 0x6fffffff
// case PT_HIOS: // 0x6fffffff /* End of OS-specific */
// case PT_LOPROC: // 0x70000000 /* Start of processor-specific */
// case PT_HIPROC: // 0x7fffffff /* End of processor-specific */
default:
return "???";
}
}
static const char* flags_to_string(uint32_t flags)
{
switch(flags)
{
case 1:
return "--x";
break;
case 2:
return "-w-";
break;
case 3:
return "-wx";
break;
case 4:
return "r--";
break;
case 5:
return "r-x";
break;
case 6:
return "rw-";
break;
case 7:
return "rwx";
break;
default:
return "???";
break;
}
}
static int header_handler(struct dl_phdr_info* info, size_t size, void* data)
{
int j;
printf("name=%s (%d segments) address=%p\n",
info->dlpi_name, info->dlpi_phnum, (void*)info->dlpi_addr);
for (j = 0; j < info->dlpi_phnum; j++) {
printf("\t\t header %2d: address=%10p\n", j,
(void*) (info->dlpi_addr + info->dlpi_phdr[j].p_vaddr));
printf("\t\t\t type=0x%X (%s),\n\t\t\t flags=0x%X (%s)\n",
info->dlpi_phdr[j].p_type,
segment_type_to_string(info->dlpi_phdr[j].p_type),
info->dlpi_phdr[j].p_flags,
flags_to_string(info->dlpi_phdr[j].p_flags));
}
printf("\n");
return 0;
}
int main(int argc, char* argv[])
{
// function from statically aware loaded library
sharedLib1Function(argc);
// function from run-time dynamically loaded library
void* pLibHandle = dlopen("libdemo2.so", RTLD_GLOBAL | RTLD_NOW);
if(NULL == pLibHandle)
{
printf("Failed loading libdemo2.so, error = %s\n", dlerror());
return -1;
}
PFUNC pFunc = (PFUNC)dlsym(pLibHandle, "sharedLib2Function");
if(NULL == pFunc)
{
printf("Failed identifying the symbol \"sharedLib2Function\"\n");
dlclose(pLibHandle);
pLibHandle = NULL;
return -1;
}
pFunc(argc);
if(2 == argc)
getchar();
if(3 == argc)
dl_iterate_phdr (header_handler, NULL);
return 0;
}
这个代码示例的核心部分属于对dl_iterate_phdr()函数的调用。这个函数本质上是在运行时提取相关的流程映射信息,并将其传递给调用者。调用者负责提供回调函数的定制实现(本例中为header_handler())。图 13-5 显示了生成的屏幕打印输出的样子。
图 13-5。
The programmatic way (relying on dl_iterate_phdr() call) of examining the dynamic library loading locations in the process memory map
创建和维护静态库
大多数与处理静态库相关的任务都可以通过使用 Linux ar archiver 来完成。完成诸如反汇编静态库代码或检查其符号之类的任务与在应用程序或动态库上执行这些任务没有什么不同。
十四、Windows 工具箱
Abstract
本章的目的是向读者介绍一套用于分析 Windows 二进制文件内容的工具(实用程序以及其他方法)。尽管 Linux objdump实用程序有一些分析 PE/COFF 格式的能力,但本章的重点将是本地的 Windows 工具,这些工具更有可能适应 PE/COFF 格式的任何变化。
本章的目的是向读者介绍一套用于分析 Windows 二进制文件内容的工具(实用程序以及其他方法)。尽管 Linux objdump实用程序有一些分析 PE/COFF 格式的能力,但本章的重点将是本地的 Windows 工具,这些工具更有可能适应 PE/COFF 格式的任何变化。
库经理(lib.exe)
Windows 32 位库管理器lib.exe是 Visual Studio 开发工具的标准部分(图 14-1 )。
图 14-1。
Using the lib.exe utility
这个实用程序不仅以与它的 Linux 对应物(archiver ar)相同的方式处理静态库,而且还在动态库领域扮演一个角色,作为可以创建导入库(DLL 符号的集合,文件扩展名.lib)以及导出文件(能够解决循环依赖,文件扩展名.exp)的工具。关于lib.exe的详细文档可以在 MSDN 网站( http://msdn.microsoft.com/en-us/library/7ykb2k5f.aspx) ).)找到
作为静态库工具的 lib.exe
在这一节中,我将举例说明lib.exe工具可能真正有用的典型角色。
lib.exe 作为默认归档工具
使用 Visual Studio 创建 C/C++ 静态库项目时,lib.exe被设置为默认的归档器/库管理器工具,项目设置的‘库管理器’选项卡用于为其指定命令行选项(图 14-2 )。
图 14-2。
Using lib.exe as default archiver
默认情况下,构建静态库项目会在编译阶段后调用lib.exe,这无需开发人员采取任何行动。然而,这不一定是使用lib.exe必须结束的地方。可以从 Visual Studio 命令提示符下运行lib.exe,就像使用 Linux ar archiver 执行相同类型的任务一样。
作为命令行工具的 lib.exe
为了说明lib.exe的用法,您将创建一个与 Linux 静态库功能完全匹配的 Windows 静态库,用于演示第十章中 ar 的用法。演示项目由四个源文件(first.c、second.c、third.c和fourth.c)和一个导出头文件组成,客户端二进制文件可以使用这个文件。这些文件显示在以下五个示例中。
file: first.c
#include "mystaticlibexports.h"
int first_function(int x)
{
return (x+1);
}
file: second.c
#include "mystaticlibexports.h"
int fourth_function(int x)
{
return (x+4);
}
file: third.c
#include "mystaticlibexports.h"
int second_function(int x)
{
return (x+2);
}
file: fourth.c
#include "mystaticlibexports.h"
int third_function(int x)
{
return (x+3);
}
file: mystaticlibexports.h
#pragma once
int first_function(int x);
int second_function(int x);
int third_function(int x);
int fourth_function(int x);
创建静态库
让我们假设您编译了所有四个源文件,并且您有四个可用的目标文件(first.obj、second.obj、third.obj和fourth.obj)。将所需的库名传递给lib.exe(在/OUT标志之后),后跟参与的目标文件列表,这将产生创建静态库的效果,如图 14-3 所示。
图 14-3。
Using lib.exe to combine object files into a static library
为了完全模仿 Visual Studio 在创建静态库项目时提供的默认设置,我添加了/NOLOGO参数。
列出静态库内容
当/LIST标志传递给lib.exe时,打印出静态库当前包含的目标文件列表,如图 14-4 所示。
图 14-4。
Using lib.exe to list the object files of static library
从静态库中移除单个目标文件
通过将/REMOVE标志传递给lib.exe,可以从静态库中移除单个目标文件(图 14-5 )。
图 14-5。
Using lib.exe to remove individual object file from static library
将目标文件插入到静态库中
通过传递库文件名,然后传递要添加的目标文件列表,可以将新的目标文件添加到现有的静态库中。这个语法非常类似于创建静态库的场景,除了可以省略/OUT标志(图 14-6 )。
图 14-6。
Using lib.exe to insert object file to static library
从静态库中提取单个目标文件
最后,可以从静态库中提取各个目标文件。为了演示它,我首先有目的地删除了计划从静态库中提取的原始目标文件(first.obj)(图 14-7 )。
图 14-7。
Using lib.exe to extract an individual object file from the static library
动态库领域中的 lib.exe(导入库工具)
lib.exe也用于创建 DLL 导入库(。lib)文件和导出文件(。exp)基于可用的导出定义文件(。def)。当严格在 Visual Studio 环境中工作时,这个任务通常会自动分配给lib.exe。当 DLL 是由第三方编译器创建的,而第三方编译器没有创建相应的导入库和导出文件时,会出现更有趣的情况。在这种情况下,lib.exe必须从命令行运行(即 Visual Studio 命令提示符)。
以下示例说明了如何在交叉编译会话后使用lib.exe创建缺失的导入库,在交叉编译会话中,运行在 Linux 上的 MinGW 编译器生成了 Windows 二进制文件,但没有提供所需的导入库(图 14-8 )。
图 14-8。
Using lib.exe to create an import library based on DLL and its definition (.DEF)_ file
垃圾箱实用程序
Visual Studio dumpbin实用程序( http://support.microsoft.com/kb/177429 )在很大程度上是 Linux objdump实用程序的 Windows 等价物,因为它执行可执行文件的重要细节的检查和分析,例如导出的符号、部分、反汇编代码(。text)节,静态库中的目标文件列表等。
该工具也是 Visual Studio 包的标准部分。类似于之前描述的lib工具,它通常从 Visual Studio 命令提示符下运行(图 14-9 )。
图 14-9。
Using the dumpbin utility
运行dumpbin可以完成以下章节中描述的典型任务。
识别二进制文件类型
在没有额外标志的情况下运行时,dumpbin报告二进制文件类型(图 14-10 )。
图 14-10。
Using the dumpbin utility to identify binary file types
列出 DLL 导出的符号
运行dumpbin /EXPORTS <dll path>提供导出符号列表(图 14-11 )。
图 14-11。
Using dumpbin utility to list exported symbols of DLL file
列出并检查各部分
运行dumpbin /HEADERS <binary file path>打印出文件中出现的完整章节列表(图 14-12 )。
图 14-12。
Using dumpbin to list the sections
一旦列出了截面名称,就可以通过运行dumpbin /SECTION:<section name> <binary file path>获得各个截面的信息(图 14-13 )。
图 14-13。
Using dumpbin to get detailed insight into a specific section
反汇编代码
运行dumpbin /DISASM <binary file path>提供完整二进制文件的反汇编列表(图 14-14 )。
图 14-14。
Using dumpbin to disassemble the code
标识调试版本
dumpbin实用程序用于识别二进制文件的调试版本。调试版本的指示器根据实际的二进制文件类型而有所不同。
目标文件
在目标文件(*.obj上运行dumpbin /SYMBOLS <binary file path>会将为调试而构建的目标文件报告为COFF OBJECT类型的文件(图 14-15 )。
图 14-15。
Using dumpbin to detect the debug version of the object file
同一文件的发布版本将被报告为文件类型匿名对象(图 14-16 )。
图 14-16。
Indication of the release built of the object file
dll 和可执行文件
DLL 或可执行文件是为调试而构建的,这一点可以从运行dumpbin /HEADERS选项的输出中的.idata部分看出。本节的目的是支持仅在调试模式下可用的“编辑并继续”功能。更具体地说,要启用该选项,需要/INCREMENTAL链接器标志,通常为调试设置,为发布配置禁用(图 14-17 )。
图 14-17。
Using dumpbin to detect the debug version of DLL
列出加载时间相关性
通过运行dumpbin /IMPORTS <binary file path>(图 14-18 )可以获得依赖库的完整列表以及从其中导入的符号。
图 14-18。
Using dumpbin to list loading dependencies
依赖行者
Dependency Walker(又名depends.exe,参见 www.dependencywalker.com/ )是一个能够跟踪已加载动态库的依赖链的实用程序(图 14-19 )。它不仅能够分析二进制文件(在这种情况下,它类似于 Linux ldd实用程序),而且还可以执行运行时分析,在运行时分析中,它可以检测和报告运行时动态加载。它最初是由史蒂夫·米勒开发的,在 VS2005 版本之前是 Visual Studio 工具套件的一部分。
图 14-19。
Using the Dependency Walker utility