gcc ld 链接器相关知识,调试指令(程序员的自我修养----链接、装载与库)

时间:2022-06-17 05:58:38

最近解决一个动态链接上的问题,因为以前从来没有接触过这方面的知识,所以恶补了一下,首先要了解gcc编译指令(makefile),ld链接器的选项(还有连接脚本section指定内存位置),熟悉查看连接状态是否成功的指令工具(其中又有elf格式,ld、elf相关指令)。然后反汇编理解动态链接的实际执行过程,总的说来这里面的东西还真很多,主要是网上比较深入资料很少,大部分博文也是一知半解,所以后续有问题应直接阅读标准文档。《程序员的自我修养----链接、装载与库》这本书详细介绍了一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现。

Makefile:

LOCAL_SRC_FILES := hello.c PREFIX := arm-anykav200-linux-uclibcgnueabi-

LOCAL_SHARED_LIBRARIES := pthread

LOCAL_MODULE := hello

CC  := $(PREFIX)gcc CPP  := $(PREFIX)g++ AS  := $(CC) LD  := $(PREFIX)ld

BUILD_DIR=./

SONAME  := $(LOCAL_MODULE).elf

CROSS_PATH      ?= /opt/arm-anykav200-crosstool/usr ARM_LIBC_PATH ?=$(CROSS_PATH)/arm-anykav200-linux-uclibcgnueabi/sysroot/usr/lib ARM_LIBGCC_PATH ?=$(CROSS_PATH)/lib/gcc/arm-anykav200-linux-uclibcgnueabi/4.8.5 CLIB =  $(ARM_LIBC_PATH)/libm.a CLIB += $(ARM_LIBC_PATH)/libc.a CLIB += $(ARM_LIBGCC_PATH)/libgcc.a CLIB += $(ARM_LIBGCC_PATH)/libgcc_eh.a

CFLAGS  := -Wall -fPIC -fno-builtin -nostdlib -mlong-calls LDFLAGS  := -p -X  -y memset -y memcpy -y mi  -Map maps.txt -T link.lds --wrap=malloc -l:../so/libmi.so -l:../ko/libko.so

C_SRC  := $(filter %.c,$(LOCAL_SRC_FILES)) C_OBJ  := $(patsubst %.c,$(BUILD_DIR)/%.o,$(C_SRC))

.PHONY: all clean

LIBC_OS := $(wildcard libc/*.os)

TARGET  := $(BUILD_DIR)/$(SONAME)

all: $(TARGET)

#主要是这里的链接顺序问题,优先链接libc的静态库,防止链接到了动态库里的libc段

$(TARGET): $(C_OBJ)  $(LD) -o $@ $^ -dn $(CLIB) -dy $(LDFLAGS)

$(C_OBJ): $(BUILD_DIR)/%.o: %.c  $(CC) $(CFLAGS) -c $< -o $@

clean:  rm -rf *.o *.so *.elf *.txt

// 学习笔记

Gcc 链接器ld选项参数解释: -shared:  该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号)。不用该标志外部程序无法连接。

相当于一个可运行文件

-fpic:  表示编译为位置独立的代码。不用此选项的话编译后的代码是位置相关的所以动态加载时是通过代码拷贝的方式来满足不同进程的须要,而不能达到真正代码段共享的目的。

-l 选项告诉编译器要使用hello这个库。奇怪的地方是动态库的名字是libhello.so,这里却使用hello. 但这样还不行。编译会出错。

In function `main': test.c:(.text+0x1d): undefined reference to `hello' collect2: ld returned 1 exit status 这是由于hello这个库在我们自己的路径中,编译器找不到。

须要使用-L选项,告诉hello库的位置 gcc test.c -lhello -L. -o test -L .告诉编译器在当前文件夹中查找库文件

-Wl选项告诉编译器将后面的参数传递给链接器。(给编译器用的)

-soname则指定了动态库的soname(简单共享名,Short for shared object name)

当要升级系统中的一个库时,并且新库的soname和老库的soname一样,用旧库链接生成的程序使用新库依然能正常运行。这个特性使得在Linux下,升级使得共享库的程序和定位错误变得十分容易。

-Wl,rpath=<your_lib_dir>选项

gcc编译链接动态库时,很有可能编译通过,但是执行时,找不到动态链接库,那是

因为-L选项指定的路径只在编译时有效,编译出来的可执行文件不知道-L选项后面的值,

当然找不到。可以用ldd <your_execute>看看是不有 ‘not found’在你链接的库后面,

解决方法是通过-Wl,rpath=<your_lib_dir>,使得execute记住链接库的位置

-nostdlib:不链接C语言的标准库 仅搜索那些在命令行上显式指定的库路径. 在连接脚本中(包含在命令行上指定的连接脚本)指定的库路径都被忽略.

-fno-builtin -fno-builtin-function  不接受没有 __builtin_ 前缀的函数作为内建函数。

-y SYMBOL'`--trace-symbol=SYMBOL'打印出所有SYMBOL出现的被连接文件的名字. 这个选项可以被多次使用. 在很多系统中,这在预先确定底线时很有必要.当你拥有一个未定义的符号,但不知道这个引用出自哪里的时候,这个选项有用.

–as-needed

只连接使用到的动态库,连接顺序从左往右

Makefile有三个非常有用的变量。分别是$@,$^,$<代表的意义分别是: $@--目标文件,$^--所有的依赖文件,$<--第一个依赖文件。

工具指令:

1、查看调用关系 arm-anykav200-linux-uclibcgnueabi-ldd hello.elf

2、readelf -r hello.elf  显示动态连接,初始的symbol是0

Relocation section '.rel.dyn' at offset 0x893c contains 3 entries:

Offset     Info    Type            Sym.Value  Sym. Name

800009e0  00000d15 R_ARM_GLOB_DAT    00000000   __wrap_malloc

800009e4  00001015 R_ARM_GLOB_DAT    00000000   mi

800009e8  00001315 R_ARM_GLOB_DAT    00000000   memset

3、readelf -s -D  hello.elf

Ndx为1表示在.text属于静态连接;Ndx是UND,表明这个符号没有在SimpleSection.o中定义,仅仅是被引用

Symbol table for image:

Num Buc:    Value  Size   Type   Bind Vis      Ndx Name

24   0: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __fsymtab_start

17   0: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __text_end

10   2: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __rtmsymtab_end

20   3: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __vsymtab_start

6   3: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __fsymtab_end

5   3: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __rt_init_start

21   4: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __vsymtab_end

15   4: 800009f8     0 NOTYPE  GLOBAL DEFAULT   9 __data_end

8   4: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __rt_init_end

14   5: 800005b0     0 NOTYPE  GLOBAL DEFAULT   1 __rodata_start

12   6: 800009f8     0 NOTYPE  GLOBAL DEFAULT   9 __bss_end

13   8: 00000000     0 FUNC    GLOBAL DEFAULT UND __wrap_malloc

4、arm-anykav200-linux-uclibcgnueabi-objdump -h -D libmi.so > mi.txt

将中间文件dump到反汇编文件中

Elf相关概念:(动态连接及执行过程)

首先理解下plt是procedure linkage table,got是global offset table。got表中存放的是外部符号的地址。plt表中存放的是函数地址

如果一个动态库函数是第一次被调用,那么plt表中是不存在该函数的地址的,通过ld库中的函数,将这个地址取出来存放到got表中,那么当第二次调用该函数时,plt表中就有了这个函数的地址,直接跳转到该地址,而不再需要去取地址,也就是动态链接。

GOT 是 data section, 是一个 table, 除专用的几个 entry,每个 entry 的内容可以再执行的时候修改;

PLT 是 text section, 是一段一段的 code,执行中不需要修改。

elf文件中的.plt .rel.dyn .rel.plt .got .got.plt的关系

.plt的作用是一个跳板,保存了某个符号在重定位表中的偏移量(用来第一次查找某个符号)和对应的.got.plt的对应的地址(延时绑定)

.rel.dyn重定向表,在程序启动时就需要重定位完成。

.rel.plt保存了重定位表的信息,可以使用lazy(延时绑定)的连接方式

.got据说是保存了elf文件本身的各个符号的偏移量,即不要动态链接,未证明

.got.plt保存了重定位地址。

比如printf是一个重定位符号,需要连接该符号时过程是这样:

main函数call  .plt段中的一个地址,这里的第一句话就是调转到.got.plt中的保存的printf的地址,如果是第一次,那么保存的地址就是.plt中的下一句话,这个下一句话就是压入这个符号在.rel.plt中的重定位表的偏移量,然后ld程序就会根据重定位表中的信息加上这个偏移量找到这个地址,保存到重定位表所指向的地址中,这个地址其实就是.got.plt段的一个地址。

第二次调用时就可以直接获取到.got.plt中保存的地址了。

附录,参考文献:

1、https://blog.csdn.net/dean_yanqing/article/details/6669352  ld链接器选项说明

2、http://www.cnblogs.com/OCaml/archive/2012/06/18/2554086.html#sec-1-6-1 链接顺序说明

3、https://www.cnblogs.com/gatsby123/p/9750187.html   https://greek0.net/elf.html elf格式说明

4、