背景:
重读《程序员的自我修养——链接、装载与库》,里面第3章主要讲目标文件。同时讲到如何将一些二进制文件作为目标文件的一个段(详细的请参考此书)。
像图片、音乐文件其实也是二进制文件(作为初级程序的我,还没有达到将一切看成二进制的境界)。本文就以此展开了一些研究,顺便复习一下binutils工具以及gdb的使用。
另外,也将这个知识应用到我的ARM开发板上,即是在原来基本上,添加图片的显示,当然,图片已经放到可执行程序中了,无须额外的图片文件。 由于不可避免地粘贴程序编译、程序运行的结果,在此作一些约定:
图片文件名称为logo.jpg,将其转换成目录文件的名称是logo_386.o,测试文件为logo_test.c。
本文有代码有真相,如果想有图有真相的话,移步到这里:将图片嵌入程序文件的测试。
1、转换
将图片文件转换成目标文件 使用objcopy工具即可将图片文件转换成目标文件,示例如下:
1 | $ objcopy -I binary -O elf32-i386 -B i386 logo.jpg logo_386.o |
这里是针对386平台,毕竟这个平台比较方便(这是废话,请无视)。其中-I选项指定输入文件的格式,这里是二进制;-O指定输出文件的格式,这里应该是指elf文件类型;-B是指定目标文件的架构,这里是i386平台,关于i386,Linux是比较笼统的说法,具体参考相关资料(真实中,笔者使用的Linux是运行在Intel i3 CPU的虚拟机中)。
用objdump看一下logo_386.o有什么东西:
1 | $ objdump -ht logo_386.o |
-ht选项表示输入段的头部和目标文件的符号,可以看到,最后三行输出了三个符号,它们分别表示图片文件在内存中的起始地址、结束地址和大小,我们可以在程序中直接声明并使用它们(来自《程序员的自我修养》)。不过,经过笔者的测试,发现这些讲述有些不正确,此是后话,暂且不提。
这三个符号是会变化的,它的命名格式是:_binary_*_start/end/size
,其中,*
是图片的文件及后缀名。如果图片是foo.bmp,那么,这三个符号就是_binary_foo_bmp_start/end/size了。如果使用十六进制查看logo_386.o,便会发现它比logo.jpg多了一些信息,这里将它们称为头部和尾部信息(头部和尾部是这里的描述,不能登大雅之堂)。头部是一个ELF头部结构体,尾部笔者就不知道了。
2、测试程序
下面是测试程序:
1 | /****************************************************************** |
注意:程序中没有给出_binary_logo_jpg_start的类型,因为笔者想不出它们几个到底是什么。如果使用-Wall来编译,得到下面的警告:
1 | logo_test.c:14: warning: type defaults to ‘int’ in declaration of ‘_binary_logo_jpg_start’ |
这说明,如果代码没有明确指定类型,编译器默认int类型。
在代码中,“符号”一词指的东西比较多,指针是符号、数组名是符号,函数名称是符号、变量名称是符号,似乎一切均符号(u-boot将“符号”用得淋漓尽致,可以参考u-boot启动部分的C代码及汇编代码,这部分的C代码就使用到了汇编中的“符号”)。
程序运行结果如下:
1 | $ ./a.out |
程序特意输出elf文件头部信息的结构体的大小,结果是52字节,而生成的目标文件中,图片内容位于0x34字节偏移,前面有0x34字节,0x34正是十进制的52。关于这个结构体,在此不展开。由于笔者曾经在一段时间中几乎天天看jpg文件的内容(主要是关于jpeg、mjpeg、avi这方面的),知道jpg文件以“FF D8”开始,在“FF D9”结束,目标文件的“FF D8”就在0x34处。
Linux下用hexdump查看开始部分内容(由于网页关系,可能不对齐):
1 | $ hexdump -C logo_386.o | head -n 4 |
3、调试程序
我们用gdb分别看看那三个符号:
先是_binary_logo_jpg_start
1 | (gdb) p/x _binary_logo_jpg_start |
可以看到,&_binary_logo_jpg_start的值为0x8049674,这表明图片内容位于0x8049674地址,按笔者对ELF文件的认识,它应该位于.text段,就是说,图片已经属于可执行文件的一部分了。_binary_logo_jpg_start值为0xe1ffd8ff,我们知道,386是小端模式,反过来一个字节一个字节看,它是ff d8 ff e1,这不是图片内容是什么?不信,我们再看一下那个地址的8个字节:
1 | (gdb) x/8b &_binary_logo_jpg_start |
_binary_logo_jpg_start
的值正是图片内容的前4个字节。
再看看_binary_logo_jpg_end:
1 | (gdb) p _binary_logo_jpg_end |
它的值为0,地址为0x804a898,我们再看看这个地址稍微前面的内容,就以0x804a890地址为例:
1 | (gdb) x/12b 0x804a890 |
我们看到几个关键的值,一是4576,它很接近图片文件的大小,另一个是0xff 0xd9,它是图片结束的标志,而0xd9,位于离_binary_logo_jpg_start地址4584处,图片大小正是4584字节。
再看看_binary_logo_jpg_size:
1 | (gdb) p _binary_logo_jpg_size |
0x11e8的十进制是4584,它就是图片文件的大小。
从上面打印的结果来,那三个符号似乎是int类型的变量,因为打印它们的地址时,如下显示:
1 | (gdb) p &_binary_logo_jpg_start |
如果是int类型的变量的话,按理说应该能打印它们的值出来的,但下面的语句:
1 | printf("%d %d %d\n", _binary_logo_jpg_start, _binary_logo_jpg_end, _binary_logo_jpg_size); |
会造成段错误,经试验,是最后一个符号_binary_logo_jpg_size造成的,这从一个侧面说明它们又不全是int类型。这是个人造成的错误(即笔者没有显式指定它们的类型)还是某种笔者未知的原因(如何从目标文件知道某个符号是什么变量?)还是其它原因,暂时没有研究到。也是因为这样,笔者才在前面说“讲述有些不正确”。在使用中,可以这样认为,&_binary_logo_jpg_start得到图片开始地址,&_binary_logo_jpg_size得到图片的大小。在这篇文章中就是这样应用的。
注:
1、关于i386,曾经的某个时候,笔者的一个同学问了笔者,但笔者答不上来。网上有论坛也有人问在Linux输入uname -a得到的那些i386、i586、i686是什么意思,现在忘了。Linux基础的书籍中应该有这方面的知识。
2、binutils的确很有用,但好像又没有什么用,笔者很久就学习了一下,结果现在又忘了。就像数据结构和算法,似乎有用,似乎又没用。这只是说:当使用到的时候,它有就用。不是有句话吗,书到用时方恨少。平时多积累点知识,还是有用的。