修改链接器脚本以使.text部分可写,错误

时间:2014-01-07 09:53:46

标签: 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

2 个答案:

答案 0 :(得分:3)

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

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

我认为这只是概念实施的证明,而不是我在现实世界的应用程序中使用过的东西。在某种高性能库中使用它看起来很诱人,但根据我的经验,更改库的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;
}

<强>功能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),利用常量的代码可能会改变大小,需要另一个重新编译重新链接。如果最后一行输出&#34;重新编译并重新链接&#34; ,只需重复最后两步,即五行。

(注意,由于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 */

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

补充说明:

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

    如果您仅使用该示例来研究如何在正在运行的进程中修改可执行代码,则无关紧要。但是,如果您在此示例的基础上构建运行时函数替换方案,则可能需要对替换的代码使用与位置无关的代码(请参阅the GCC manual了解您的体系结构的相关选项)或执行您自己的重定位。 / p>

  • 如果另一个线程或信号处理程序执行了被修改的代码,那么您将遇到严重问题。你得到未定义的结果。不幸的是,一些库启动额外的线程,这可能不会阻止所有可能的信号,因此在修改可能由信号处理程序运行的代码时要格外小心。

  • 不要假设编译器以特定方式编译代码或使用特定组织。我的示例使用单独的编译单元,以避免编译器可能在相似函数之间共享代码的情况。

    此外,它直接检查最终的可执行二进制文件,以获取要修改的大小和地址以修改整个函数实现。所有验证都应该在目标文件或最终可执行文件和反汇编上完成,而不是仅仅查看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通用内核在x86-64上运行。

答案 1 :(得分:0)

如果您只想拥有一个带有可写.text的可执行文件,则只需链接-N

即可

至少对我来说,binutils 2.22,ld -N objectfile.o

将生成一个我可以愉快地写的二进制文件。

读取gcc页面,您可以通过以下方式从gcc传递链接器选项:gcc -XN source