【问题标题】:Computing the set of writes when executing a function执行函数时计算写入集
【发布时间】:2019-07-08 19:38:11
【问题描述】:

我想编写一个函数 computeWriteSet,它接受任意函数 f 作为参数,并且 (1) 执行函数 f 并且 (2) 返回修改后的 places 集合或在f 执行期间写入(地址/页面/对象)。

writeset computeWriteSet(function f) {
  writeset ws = createEmptyWriteset();
  // prepare to execute f
  startRecordingWrites(&ws);
  f();
  stopRecordingWrites(&ws);
  // post-process the write-set
  return ws;
}
  1. 有哪些实施方案可供选择?
  2. 它们的权衡是什么(在哪种情况下哪种实现更有效,有什么限制?)

注意事项

函数在运行时指定,可以做任何事情(即可以包含任何指令集,包括循环、分支和函数/系统调用。

应该记录从调用f 到它返回的所有写入(这包括从f 本身调用的函数)。为简单起见,我们假设 computeWriteSet 没有从内部调用。

特定于操作系统的技巧是允许的(并且可能是必需的)。我对 Linux 尤其感兴趣,最好是在用户空间中。

示例

static int x = 0;
static int y = 0;
static int z = 0;

void a() {
  if (y) z++;
  if (x) y++;
  x = (x + 1) % 2;
}

int main() {
  computeWriteSet(a); // returns { &x }     => {x,y,z} = {1, 0, 0}
  computeWriteSet(a); // returns { &x, &y } => {x,y,z} = {0, 1, 0}
  computeWriteSet(a); // returns { &x, &z } => {x,y,z} = {1, 1, 1}
  return 0;
}

预期输出

输出应该是一组更改。这可以是页面集:

{ <address of x>, <address of y>, …}

或者内存地址集:

{<page of x and y>, <page of z>, …}

或对象集((基于分配函数的插入)

x = malloc(100) // returns address 0xAAA
y = malloc(200) // returns address 0xBBB
…

{ {address, size}, {0xAAA, 100}, {0xBBB, 200}, … }

返回值是有意松散指定的——不同的技术会有不同的空间分辨率和不同的开销。

请注意:

这是一个非常罕见的编程问题,因此,如果您认为应该关闭它,请告诉我为什么,理想情况下,如何措辞/​​放置它以使其遵循指南。 :-)

【问题讨论】:

  • valgrind/memcheck的源代码可能包含你想要的。
  • 我怀疑这些程序的工作方式是使用mprotect 将内存标记为只读。然后任何内存修改都会触发一个信号,并且处理程序会跟踪它试图修改的内存。
  • 继续@Barmar 的想法。在检测到写入只读页面后,您需要设置保护以写入并重复操作(这是调试器在监视写入时通常会执行的操作)。不过这会非常慢。
  • 没有什么好方法可以满足您的要求。它需要对代码进行检测,类似于 valgrind 或其他工具。权衡将是运行时性能和额外的编译或运行步骤。开发(或重用)此功能也很头疼。
  • 如果您正在寻找可以帮助您完成任务的工具,那么您的问题与“软件推荐”问题无关。如果你想收集ideas,那么问题就是“open-ended”。如果你想得到一个完全描述实现你想要的方法的答案,那么对于 Stack Overflow 格式,这样的答案将是 太长。您需要通过选择检测工具来缩小问题的范围,并专门询问如何将此工具应用于特定任务。

标签: c linux memory linux-kernel operating-system


【解决方案1】:

正如@Barmar 所建议的,实现此目的的一种方法是通过mprotect

这将为每个内存页面生成一个异常,这可能会增加相当大的开销,具体取决于函数。这个异常由我们处理,然后我们将对应的地址插入到一个集合中。

一个 100 行的小型 C++/C 完整的杂乱程序展示了这一点。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <ucontext.h>
#include <fcntl.h>
#include <execinfo.h>
#include <sys/mman.h>

#include <set>
#include <functional>
#include <cassert>

extern "C" {
extern int __data_start;
extern int _end;
}

#define PAGE_SIZE sysconf(_SC_PAGESIZE)
#define PAGE_MASK (PAGE_SIZE - 1)
#define PAGE_ALIGN_DOWN(x) (((intptr_t) (x)) & ~PAGE_MASK)
#define PAGE_ALIGN_UP(x) ((((intptr_t) (x)) + PAGE_MASK) & ~PAGE_MASK)
#define GLOBALS_START PAGE_ALIGN_DOWN((intptr_t) &__data_start)
#define GLOBALS_END   PAGE_ALIGN_UP((intptr_t) &_end - 1)
#define GLOBALS_SIZE  (GLOBALS_END - GLOBALS_START)

std::set<void*> *addresses = new std::set<void*>();

void sighandler(int signum, siginfo_t *siginfo, void *ctx) {
    void *addr = siginfo->si_addr;
    void *aligned_addr = reinterpret_cast<void*>(PAGE_ALIGN_DOWN(addr));
    switch(siginfo->si_code) {
    case SEGV_ACCERR:
        mprotect(aligned_addr, PAGE_SIZE, PROT_READ | PROT_WRITE);
        addresses->insert(aligned_addr);
        break;
    default:
        exit(-1);
    }
}

void computeWriteSet(std::function<void()> f) {
    static bool initialized = false;
    if (!initialized) {
        // install signal handler
        stack_t sigstk;
        sigstk.ss_sp = malloc(SIGSTKSZ);
        sigstk.ss_size = SIGSTKSZ;
        sigstk.ss_flags = 0;
        sigaltstack(&sigstk, NULL);
        struct sigaction siga;
        sigemptyset(&siga.sa_mask);
        sigaddset(&siga.sa_mask, SIGSEGV);
        sigprocmask(SIG_BLOCK, &siga.sa_mask, NULL);
        siga.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART | SA_NODEFER;
        siga.sa_sigaction = sighandler;
        sigaction(SIGSEGV, &siga, NULL);
        sigprocmask(SIG_UNBLOCK, &siga.sa_mask, NULL);
        initialized = true;
    }
    addresses->clear();
    printf("\nexecuting function\n");
    printf("--------------\n");
    mprotect(reinterpret_cast<void*>(GLOBALS_START), GLOBALS_SIZE, PROT_READ);
    f();
    mprotect(reinterpret_cast<void*>(GLOBALS_START), GLOBALS_SIZE, PROT_READ | PROT_WRITE);
    printf("--------------\n");
    printf("pages written:\n");
    for (auto addr : *addresses) {
        printf("%p\n", addr);
    }
}

void f() {
    static int x[1024] = {0};
    static int y[1024] = {0};
    static int z[1024] = {0};
    static bool firsttime = true;
    if (firsttime) {
        printf("&x[0] = %p\n&y[0] = %p\n&z[0] = %p\n", x, y, z);
        firsttime = false;
    }
    if (y[0]) z[0]++;
    if (x[0]) y[0]++;
    x[0] = (x[0] + 1) % 2;
    printf("{x, y, z} = {%d, %d, %d}\n", x[0], y[0], z[0]);
}

int main() {
    computeWriteSet(f);
    computeWriteSet(f);
    computeWriteSet(f);
    return 0;
}

使用g++ --std=c++11 example.cpp编译。

执行打印如下:

executing function
--------------
&x[0] = 0x6041c0
&y[0] = 0x6051c0
&z[0] = 0x6061c0
{x, y, z} = {1, 0, 0}
--------------
pages written:
0x604000

executing function
--------------
{x, y, z} = {0, 1, 0}
--------------
pages written:
0x604000
0x605000

executing function
--------------
{x, y, z} = {1, 1, 1}
--------------
pages written:
0x604000
0x606000

一些注意事项:

  • 我们使xyz 足够大的数组(大小为PAGE_SIZE/sizeof(int),在我的机器上为1024),以便它们位于不同的内存页中,因此可以区分.

  • 此程序仅适用于全局/静态变量,因此它可以很短。正如@AShelly 所建议的,要将其扩展为使用堆和其他内存映射,可以通过插入来完成。

主题跟进:有什么方法可以避免O(N) 信号,其中N 是写入的页数?

【讨论】:

    猜你喜欢
    • 2021-07-15
    • 2015-07-19
    • 2011-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-08
    • 1970-01-01
    • 2018-09-17
    相关资源
    最近更新 更多