Mach-O查找内存中符号表地址、字符串表地址的计算过程中的基址的真正意义

664 阅读5分钟

我们首先来看KSCrash中的KSDynamicLinker 文件中的一个函数,代码如下:

/** Get the segment base address of the specified image.
 *
 * This is required for any symtab command offsets.
 *
 * @param idx The image index.
 * @return The image's base address, or 0 if none was found.
 */
static uintptr_t segmentBaseOfImageIndex(const uint32_t idx)
{
    // image list可以看到image列表,上面有对应的index
    const struct mach_header* header = _dyld_get_image_header(idx);

    // Look for a segment command and return the file image address.
    uintptr_t cmdPtr = firstCmdAfterHeader(header);

    if(cmdPtr == 0)
    {
        return 0;
    }

    for(uint32_t i = 0;i < header->ncmds; i++)
    {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;

        if(loadCmd->cmd == LC_SEGMENT)
        {
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;

            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0)
            {
                //虚拟地址偏移量 = 虚拟地址(vmaddr) – 文件偏移量(fileoff)

                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        }

        else if(loadCmd->cmd == LC_SEGMENT_64)
        {
            const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;

            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0)
            {
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        cmdPtr += loadCmd->cmdsize;
    }
    return 0;
}

该函数被如此调用:

const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;

最后的使用这个基地址计算符号表地址、字符串表地址在内存中的运行时地址

image.png

问题

这个segmentBase基址代表的是什么? 大量资料说这个值是__LINKEDIT 段在内存中的基址,有的认为是当前 image 在内存中的基址。其实都不是,下面是我的论证;

论证

以下需要Mach-O的知识,不熟悉的麻烦先熟悉;

分析过程

对于每一个 segment 而言,设置进程虚拟内存的过程就是将相应的Mach-O的内容加载到内存中,也就是从 Mach-O 文件的 fileoff 初加载 filesize 字节到虚拟内存地址的 vmaddr 处,占用 vmsize 字节。需要留意,对某些 segment 来说,vmsize 可能会大于 filesize,如__Data、__LINKEDIT。

如下图所示:

image.png 在加载__LINKEDIT的时候,会从Mach-O的81920的位置开始加载50912的字节到虚拟内存的4295065600+slider(ASLR偏移)的位置,占用65536字节;

我的测试demo的Mach-O的各__DATA和__LINKEDIT segment的vmszie比fileszie大,其他的segment两个值是一样大的,如下图:

image.png

下面来个图说明一下我的demo的Mach-O加载到虚拟内存中的分布和Mach-O文件的布局的对比;

image.png

上部分是代表Mach-O加载到虚拟内存的布局,下部分是对应的Mach-O文件的布局,其中灰色代表虚拟内存(下部分的灰色代表对应到内存大小多出来的部分),各个颜色块代表不同的segment,我们留意到符号表、字符串表在 Mach-O 文件的位置,位于 __LINKEDIT 段中。vmaddr为__LINKEDIT段的起始运行时地址,sym_vmaddr为符号表的起始运行时地址,fileoff为__LINKEDIT 段的偏移量,symoff为符号表的偏移量,str_off为字符串表的偏移量;

由图可得,灰色2和3的大小是一致的,因为__DATA段的虚拟内存大小比该段大的原因。我们假设1的大小和2、3的大小一致,又有以下算式的转换

const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
//上式子转换成
const uintptr_t segmentBase = segmentCmd->vmaddr - segmentCmd->fileoff + imageVMAddrSlide

可知segmentBase其实就是1的末尾地址,如上图所示,即是 segmentBase = __TEXT段的起始地址+slider(ASLR偏移)+__LINKEDIT之前的所有段的虚拟内存大小与自身大小的差之和

比如我的demo,因为只有__DATA段的vmszie比fileszie大,大了灰色2那么大的值(16384),既有segmentBase = __TEXT段的起始地址+slider(ASLR偏移)+2的大小(16384),既为1的末尾地址;

可以看到,因为符号表、字符串表在 Mach-O 文件的位置,位于 __LINKEDIT 段中,整个段是一起加载到内存中,所以有 符号表的地址:sym_vmaddr = segmentBase + 符号表的偏移大小 = segmentBase + symoff;字符串表的地址:sym_vmaddr = segmentBase + 字符串表的偏移大小 = segmentBase + str_off

这里也可以换一种思路来思考,竟然__LINKEDIT段是整体加载入内存的,那么__LINKEDIT段的虚拟地址和符号表、字符串表的虚拟地址应该是差了一个固定值,这个值即是符号表/字符串表的偏移值 - __LINKEDIT段的偏移值,则符号表有以下公式:sym_vmaddr = vmaddr + slider + (symoff - fileoff) = vmaddr + slider - fileoff + symoff = segmentBase + symoff,字符串表推理一致;

思考: 如果__LINKEDIT段前面的所有段的vmszie和fileszie一样大,则就有segmentBase是当前 image 在内存中的基址,即__TEXT段的起始地址了;

验证

下面是这个结论的验证

image.png

image.png 通过代码为分别打印了以下的地址

__TEXT段的起始地址(虚拟地址):__TEXT vmaddr = 4294967296

_LINKEDIT段起始地址(虚拟地址):SEG_LINKEDIT vmaddr = 4295065600

_LINKEDIT段的偏移量:fileoff = 81920

基址(运行时地址):segmentBase = 4372709376

slider值:imageVMAddrSlide = 77725696

image的名称:imageName

符号表的偏移值:symbolTableOff = 85152

字符串表的偏移值:stringTableOff = 96920

符号表的地址(运行时地址):symbolTableAdder = 4372794528

字符串表的地址(运行时地址):stringTableAdder = 4372806296

:运行时地址 = 虚拟地址 + slider值

由打印的数据可计算得

segmentBase ≠ __TEXT段的起始地址 + slider值 = 4294967296 + 77725696 = 4372692992

segmentBase = __TEXT段的起始地址 + slider值 + __DATA段的vmszie和fileszie的差值 = 4294967296 + 77725696 + 16384 = 4372709376

由此可验证:segmentBase = __TEXT段的起始地址+slider(ASLR偏移)+__LINKEDIT之前的所有段的虚拟内存大小与自身大小的差之和