c - 修改链接描述文件以使.text节可写,错误

标签 c linux gcc linker x86-64

我试图使.text节对于C程序可写。我浏览了this SO question中提供的选项,并在修改链接器脚本以实现此目标时归零。

  • 为此,我使用
  • 创建了可写内存区域
    MEMORY { rwx (wx) : ORIGIN = 0x400000, LENGTH = 256K}
    并在.text部分添加:
    .text           :
      {
     *(.text.unlikely .text.*_unlikely)
     *(.text.exit .text.exit.*)
     *(.text.startup .text.startup.*)
     *(.text.hot .text.hot.*)
     *(.text .stub .text.* .gnu.linkonce.t.*)
     /* .gnu.warning sections are handled specially by elf32.em.  */
     *(.gnu.warning)
    } >rwx
    

    在使用gcc标志-T编译代码并将链接器文件作为参数时,出现错误:
    error: no memory region specified for loadable section '.interp'
    

    我只是试图更改.text区域的内存权限。在Ubuntu x86_64架构上工作。

    有一个更好的方法吗?
    非常感谢您的帮助。

    谢谢

    Linker Script

    Linker Script on pastie.org

    最佳答案

    在Linux中,您可以使用mprotect()从运行时代码启用/禁用文本节写保护。请参阅 man 2 mprotect 中的“注释”部分。

    这是一个真实的例子。首先,请注意:

    我认为这只是概念实现的证明,而不是我在现实应用程序中曾经使用的东西。在某种高性能的库中使用它可能看起来很诱人,但是以我的经验,更改库的API(或范例/方法)通常会产生更好的结果-以及更少的难以调试的错误。

    请考虑以下六个文件:

    foo1.c :

    int foo1(const int a, const int b) { return a*a - 2*a*b + b*b; }
    

    foo2.c :
    int foo2(const int a, const int b) { return a*a + b*b; }
    

    foo.h.header :
    #ifndef   FOO_H
    #define   FOO_H
    
    extern int foo1(const int a, const int b);
    
    extern int foo2(const int a, const int b);
    

    foo.h.footer :
    #endif /* FOO_H */
    

    main.c :
    #include <unistd.h>
    #include <sys/mman.h>
    #include <errno.h>
    
    #include <string.h>
    #include <stdio.h>
    #include "foo.h"
    
    int text_copy(const void *const target,
                  const void *const source,
                  const size_t      length)
    {
        const long  page = sysconf(_SC_PAGESIZE);
        void       *start = (char *)target - ((long)target % page);
        size_t      bytes = length + (size_t)((long)target % page);
    
        /* Verify sane page size. */
        if (page < 1L)
            return errno = ENOTSUP;
    
        /* Although length should not need to be a multiple of page size,
         * adjust it up if need be. */
        if (bytes % (size_t)page)
            bytes = bytes + (size_t)page - (bytes % (size_t)page);
    
        /* Disable write protect on target pages. */
        if (mprotect(start, bytes, PROT_READ | PROT_WRITE | PROT_EXEC))
            return errno;
    
        /* Copy code.
         * Note: if the target code is being executed, we're in trouble;
         *       this offers no atomicity guarantees, so other threads may
         *       end up executing some combination of old/new code.
        */
        memcpy((void *)target, (const void *)source, length);
    
        /* Re-enable write protect on target pages. */
        if (mprotect(start, bytes, PROT_READ | PROT_EXEC))
            return errno;
    
        /* Success. */
        return 0;
    }
    
    int main(void)
    {
        printf("foo1(): %d bytes at %p\n", foo1_SIZE, foo1_ADDR);
        printf("foo2(): %d bytes at %p\n", foo2_SIZE, foo2_ADDR);
    
        printf("foo1(3, 5): %d\n", foo1(3, 5));
        printf("foo2(3, 5): %d\n", foo2(3, 5));
    
        if (foo2_SIZE < foo1_SIZE) {
            printf("Replacing foo1() with foo2(): ");
            if (text_copy(foo1_ADDR, foo2_ADDR, foo2_SIZE)) {
                printf("%s.\n", strerror(errno));
                return 1;
            }
            printf("Done.\n");
        } else {
            printf("Replacing foo2() with foo1(): ");
            if (text_copy(foo2_ADDR, foo1_ADDR, foo1_SIZE)) {
                printf("%s.\n", strerror(errno));
                return 1;
            }
            printf("Done.\n");
        }
    
        printf("foo1(3, 5): %d\n", foo1(3, 5));
        printf("foo2(3, 5): %d\n", foo2(3, 5));
    
        return 0;
    }
    

    function-info.bash :
    #!/bin/bash
    
    addr_prefix=""
    addr_suffix="_ADDR"
    
    size_prefix=""
    size_suffix="_SIZE"
    
    export LANG=C
    export LC_ALL=C
    
    nm -S "$@" | while read addr size kind name dummy ; do
        [ -n "$addr" ] || continue
        [ -n "$size" ] || continue
        [ -z "$dummy" ] || continue
        [ "$kind" = "T" ] || continue
        [ "$name" != "${name#[A-Za-z]}" ] || continue
        printf '#define %s ((void *)0x%sL)\n' "$addr_prefix$name$addr_suffix" "$addr"
        printf '#define %s %d\n' "$size_prefix$name$size_suffix" "0x$size"
    done || exit $?
    

    记得使用chmod u+x ./function-info.bash使它可执行

    首先,使用有效大小但无效地址来编译源:
    gcc -W -Wall -O3 -c foo1.c
    gcc -W -Wall -O3 -c foo2.c
    ( cat foo.h.header ; ./function-info.bash foo1.o foo2.o ; cat foo.h.footer) > foo.h
    gcc -W -Wall -O3 -c main.c
    

    大小正确,但地址不正确,因为代码尚未链接。相对于最终二进制文件,通常在链接时重新定位目标文件的内容。因此,链接源以获取示例可执行文件示例:
    gcc -W -Wall -O3 main.o foo1.o foo2.o -o example
    

    提取正确的(大小和)地址:
    ( cat foo.h.header ; ./function-info.bash example ; cat foo.h.footer) > foo.h
    

    重新编译并链接,
    gcc -W -Wall -O3 -c main.c
    gcc -W -Wall -O3 foo1.o foo2.o main.o -o example
    

    并验证常量现在是否匹配:
    mv -f foo.h foo.h.used
    ( cat foo.h.header ; ./function-info.bash example ; cat foo.h.footer) > foo.h
    cmp -s foo.h foo.h.used && echo "Done." || echo "Recompile and relink."
    

    由于高度优化(-O3),使用常量的代码可能会更改大小,从而需要进行另一次重新编译-重新链接。如果最后一行输出“重新编译并重新链接”,则只需重复最后两个步骤,即五行。

    (请注意,由于foo1.c和foo2.c不在foo.h中使用常量,因此显然不需要重新编译它们。)

    在x86_64(GCC-4.6.3-1ubuntu5)上,运行./example输出
    foo1(): 21 bytes at 0x400820
    foo2(): 10 bytes at 0x400840
    foo1(3, 5): 4
    foo2(3, 5): 34
    Replacing foo1() with foo2(): Done.
    foo1(3, 5): 34
    foo2(3, 5): 34
    

    这表明foo1()函数确实已被替换。注意,较长的函数总是被较短的函数代替,因为我们不能覆盖这两个函数之外的任何代码。

    您可以修改这两个功能以验证这一点。只要记住要重复整个过程即可(以便在main()中使用正确的_SIZE和_ADDR常量)。

    仅出于傻笑,这是上面生成的foo.h:
    #ifndef   FOO_H
    #define   FOO_H
    
    extern int foo1(const int a, const int b);
    
    extern int foo2(const int a, const int b);
    #define foo1_ADDR ((void *)0x0000000000400820L)
    #define foo1_SIZE 21
    #define foo2_ADDR ((void *)0x0000000000400840L)
    #define foo2_SIZE 10
    #define main_ADDR ((void *)0x0000000000400610L)
    #define main_SIZE 291
    #define text_copy_ADDR ((void *)0x0000000000400850L)
    #define text_copy_SIZE 226
    #endif /* FOO_H */
    

    您可能希望使用更智能的脚本,例如一个awk,它使用nm -S获取所有函数名称,地址和大小,并且在头文件中仅替换现有定义的值,以生成头文件。我会使用一个Makefile和一些辅助脚本。

    进一步说明:
  • 功能代码按原样复制,不执行重定位等操作。 (这意味着,如果替换函数的机器代码包含绝对跳转,则将继续执行原始代码。选择了这些示例函数,因为它们不太可能包含绝对跳转。运行objdump -d foo1.o foo2.o从程序集中进行验证。 )

    如果使用该示例只是为了研究如何在运行的进程中修改可执行代码,则这是无关紧要的。但是,如果在此示例的顶部构建运行时功能替换方案,则可能需要对替换的代码使用位置无关的代码(有关体系结构的相关选项,请参见the GCC manual)或自行进行重定位。
  • 如果另一个线程或信号处理程序执行了正在修改的代码,则您将面临严重麻烦。您得到不确定的结果。不幸的是,某些库启动了额外的线程,这些线程可能不会阻塞所有可能的信号,因此在修改可能由信号处理程序运行的代码时要格外小心。
  • 不要假定编译器以特定方式编译代码或使用特定组织。我的示例使用单独的编译单元,以避免编译器可能在相似函数之间共享代码的情况。

    此外,它直接检查最终的可执行二进制文件,以获取要修改的大小和地址,以修改整个功能实现。所有验证都应在目标文件或最终可执行文件上进行,然后反汇编,而不仅仅是查看C代码。
  • 将依赖于地址和大小常量的任何代码放入单独的编译单元中,可以更轻松,更快地重新编译和重新链接二进制文件。 (您只需要直接重新编译使用常量的代码,甚至可以对该代码进行较少的优化,以消除额外的重新编译-重新链接周期,而不影响整体代码质量。)
  • 在我的main.c中,提供给mprotect()的地址和长度都是页面对齐的(基于用户参数)。文件说只有地址。由于保护是页面粒度的,因此确保长度是页面大小的倍数不会受到影响。
  • 您可以阅读和解析/proc/self/maps(这是内核生成的伪文件;有关更多信息,请参见 man 5 proc /proc/[pid]/maps部分)以获取现有映射及其对当前进程的保护。

  • 无论如何,如果您有任何疑问,我们很乐意尝试澄清以上内容。

    附录:

    事实证明,使用GNU扩展名 dl_iterate_phdr() 可以轻松地对所有文本部分启用/禁用写保护:
    #define  _GNU_SOURCE
    #include <unistd.h>
    #include <dlfcn.h>
    #include <sys/mman.h>
    #include <link.h>
    
    static int do_write_protect_text(struct dl_phdr_info *info, size_t size, void *data)
    {
        const int protect = (data) ? PROT_READ | PROT_EXEC : PROT_READ | PROT_WRITE | PROT_EXEC;
        size_t    page;
        size_t    i;
    
        page = sysconf(_SC_PAGESIZE);
    
        if (size < sizeof (struct dl_phdr_info))
            return ENOTSUP;
    
        /* Ignore libraries. */
        if (info->dlpi_name && info->dlpi_name[0] != '\0')
            return 0;
    
        /* Loop over each header. */
        for (i = 0; i < (size_t)info->dlpi_phnum; i++)
            if ((info->dlpi_phdr[i].p_flags & PF_X)) {
                size_t ptr = (size_t)info->dlpi_phdr[i].p_vaddr;
                size_t len = (size_t)info->dlpi_phdr[i].p_memsz;
    
                /* Start at the beginning of the relevant page, */
                if (ptr % page) {
                    len += ptr % page;
                    ptr -= ptr % page;
                }
    
                /* and use full pages. */
                if (len % page)
                    len += page - (len % page);
    
                /* Change protections. Ignore unmapped sections. */
                if (mprotect((void *)ptr, len, protect))
                    if (errno != ENOMEM)
                        return errno;
            }
    
        return 0;
    }
    
    int write_protect_text(int protect)
    {
        int result;
    
        result = dl_iterate_phdr(do_write_protect_text, (void *)(long)protect);
    
        if (result)
            errno = result;
        return result;
    }
    

    这是一个示例程序,可用于测试上述write_protect_text()函数:
    #define  _POSIX_C_SOURCE 200809L
    
    int dump_smaps(void)
    {
        FILE   *in;
        char   *line = NULL;
        size_t  size = 0;
    
        in = fopen("/proc/self/smaps", "r");
        if (!in)
            return errno;
    
        while (getline(&line, &size, in) > (ssize_t)0)
            if ((line[0] >= '0' && line[0] <= '9') ||
                (line[0] >= 'a' && line[0] <= 'f'))
                fputs(line, stdout);
    
        free(line);
    
        if (!feof(in) || ferror(in)) {
            fclose(in);
            return errno = EIO;
        }
    
        if (fclose(in))
            return errno = EIO;
    
        return 0;
    }
    
    int main(void)
    {
        printf("Initial mappings:\n");
        dump_smaps();
    
        if (write_protect_text(0)) {
            fprintf(stderr, "Cannot disable write protection on text sections: %s.\n", strerror(errno));
            return EXIT_FAILURE;
        }
    
        printf("\nMappings with write protect disabled:\n");
        dump_smaps();
    
        if (write_protect_text(1)) {
            fprintf(stderr, "Cannot enable write protection on text sections: %s.\n", strerror(errno));
            return EXIT_FAILURE;
        }
    
        printf("\nMappings with write protect enabled:\n");
        dump_smaps();
    
        return EXIT_SUCCESS;
    }
    

    该示例程序在更改文本节写保护之前和之后转储/proc/self/smaps,表明它确实确实在所有文本节上启用/禁用写保护(程序代码)。它不会尝试更改动态加载的库上的写保护。经过测试,可以使用Ubuntu 3.8.0-35-generic内核在x86-64上运行。

    关于c - 修改链接描述文件以使.text节可写,错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20968542/

    相关文章:

    linux - 使用 RPATH 构建 OpenSSL?

    c++ - Eclipse Cdt Indexer - 如何定位语法错误和未解析的名称

    gcc - 使用 gcc 为 32 位体系结构编译的 C 程序的意外退出代码

    c++ - gcc 链接器描述文件强制符号位于特定地址

    c - 如何在 C 中使用动态矩阵

    c - CUDA 中的简单缩减程序

    c - 我应该做 K&R 中的所有练习吗?

    linux - 为 mvn jetty :run 生成 System V 初始化脚本

    linux - 在 Linux 上搜索进程的内存

    c - C语言的二进制数转换