最近一个名词——懒加载,是一种与动态链接相关的技术,我对它有点感兴趣,于是决定深入了解一番。
懒加载是一种延迟加载资源的策略,它将资源的加载推迟到在首次访问或需要时才执行。这意味着在应用程序启动时,并不会立即加载和链接所有可能需要的库或模块,而是等到应用程序执行到实际需要使用它们的代码路径时才加载和链接。这可以减少启动时间和内存占用,因为应用程序只加载了实际需要的部分。
比如,以下面的程序为例。当程序刚运行到main函数时,got中还没有printf函数的地址。当程序要调用printf函数的时候,才会将printf的地址写入got中
#include
int main() {
printf("Hello World!\n");
return 0;
}
我听说Linux中默认采用懒加载,就将上面的程序编译后进行调试。
gcc main.c -o main -g
gdb main
printf函数参数只有字符串,此时编译器会将printf函数替换为puts函数。因为采用的是动态链接,要使用puts函数,会先跳转到plt,再从plt跳转到got中的地址。
先不急着运行,我们来看看got中的数据。
好奇怪,got中已经有了puts的地址。说好的懒加载呢?
那么got中什么时候被写入了puts的地址呢?
我重新调试一次。starti命令会开始执行程序,并在第一条指令处停下来。0x555555557fd0是got中puts的地址,一开始的值是0x1030。watch指令在检测到地址中的值改变后会停止运行。
这里可以看到,程序在…/sysdeps/x86_64/dl-machine.h中停了下来。程序现在还处于/lib64/ld-linux-x86-64.so.2中,这是linux的动态链接器,负责动态库加载,符号解析和重定位等。
从上面的调试过程可以看出,linux的动态链接并不是默认采用懒加载。
那怎么才能让linux动态链接的时候采用懒加载呢?需要在编译的时候加上特别的参数。
gcc main.c -o main -g -Wl,-z,lazy
这样就可以让linux以懒加载的方式解析符号地址。-z,lazy是把lazy参数传递给链接器。从这里我们可以看出,是否采用懒加载是由链接器决定的。
重新调试,得到下面的结果。
可以看到got中puts的地址还未被修改成正确的地址。那么这个0x0000555555555030又是什么呢。
上面的图是通过objdump得到的数据,0x0000555555555030对应的是0x1030,0x555555555020对应的是0x1020。
我们的跳转顺序为 main -> puts@plt -> .plt。0x555555555026会跳转到动态链接器,由动态链接器完成动态库加载和符号解析,并把解析后的地址填入got中。
这就是整个懒加载的过程了。
RELRO(Read-Only Relocations)是一种用于加强可执行文件的安全性的机制,特别是针对共享库(动态链接库)的。它的主要目的是防止一些常见的攻击技巧,例如覆盖全局偏移表(GOT)和过程链接表(PLT)的攻击。
RELRO 的作用和原理:
我们怎么知道一个可行性文件是Partial RELRO还是Full RELRO?可以通过checksec来查看。
sudo apt install checksec
可以看到懒加载是Partial RELRO,非懒加载是Full RELRO。
如果不采用懒加载,在程序正式运行前,会由动态链接器ld.so进行动态库加载和符号解析,并把dynamic和got等段设置为只读。
如果采用懒加载,只会将dynamic和部分got设置为只读,另一部分got设置为可写。
那么这种机制是如何实现的呢?我们需要通过objdump查看可执行文件的段偏移来理解。
可以看到,在懒加载中,有两个got段:.got和.got.plt。.got的内容会在程序运行前被修改,然后会把从rodata到got的读写权限设置为只读。另外,.got.plt的起始地址是4K对齐的,.got.plt和.data在一个页面中,它们的读写权限也一样,都是可读可写。在运行过程中,动态链接器会动态解析符号地址并填入.got.plt中。
而非懒加载中,.got的内容会在程序运行前被修改,然后会把从rodata到got的权限设置为只读。
本篇博客主要讲了如何让可执行文件以懒加载的方式执行,懒加载的大致流程,RELRO的原理。