在运行时检查C/C++指针是否指向只读内存(在Linux OS中)

y53ybaqx  于 2023-11-17  发布在  Linux
关注(0)|答案(1)|浏览(82)

回到过去,低级C/C++代码通过执行以下操作来识别指针ptr是否指向Linux OS中的只读存储器

extern char etext, edata;
...
(ptr > &etext) && (ptr < &edata);

字符串
事实上,我时不时地还能看到这一点。
然而,在对去年其他人发布的问题(Why does subtracting the value of etext from edata not give me the correct size for my text segment)的回答中,表明现在传统的连续内存布局text -> data -> bss -> heap -> ... -> stack -> kernel不再有保证。
这对我来说是相当令人困惑的,因为正如我所说,我看到上面的代码今天仍然在使用。但也因为这个链接问题的OP可以理解地给答案留下了一个评论,询问他们现在在哪里可以找到关于内存部分布局的信息-但答案作者不知道。这是一个非常相关的技术信息,但是我也找不到关于它的进一步信息,这些信息可以帮助我Assert上面的代码是否仍然有用,或者如何根据现代内存段布局规则修改它。
This answer到一个稍微不同的问题建议以下检查指针是否在静态变量内存中-以解决现在内存部分的顺序可能是随机的这一事实:

(void*)(x) <= (void*)&end || (void*)(x) <= (void*)&edata


基本原理是,只要检查指针位于BSS和只读数据的末尾之前,就足以确认静态内存中的指针。
因此,我的主体质询分为两部分:

  • 据我所知,上面的后一种方法是默默地假设,尽管堆的位置可能是随机的,但它位于BBS只读数据之后-永远不会在两者之间。
  • 如果是这样,并且知道文本代码段至少是第一位的,我们是否可以调整上述方法以确保检测到指针具体在只读存储器数据中(不包括BSS),在现代Linux操作系统中?我知道没有标准的通用解决方案,但现在我只想解决Linux系统的问题(尽管Mac OS的解决方案可能非常相似,因为编译器经常为这些OS提供edata,etext和end的等价物)。

我的尝试是:

#include <iostream>
extern char etext, edata;

// the following is supposed to detect whether a pointer is in the Stack
// according to this answer:
https://stackoverflow.com/questions/35206343/is-it-possible-to-identify-whether-an-address-reference-belongs-to-static-heap-s
void *stack_bottom;
bool __attribute__((noinline)) inStack(void *x) {
    void *stack_top = &stack_top;
    return x <= stack_bottom && x >= stack_top;
}

// then my attempt at checking if a pointer is in read-only data,
// excluding the BSS segment:
bool roMem(void* c) {
    if(&etext < &edata && &end > &edata)
    {
        return (c > &etext) && (c < &edata) && (!inStack((void*)c));
    } else if(&etext > &edata && &end > &edata)
    {
        return (void*)(c) <= (void*)&edata && !inStack((void*)c);
    } else if(&end < &edata)
    {
        return (void*)(c) > (void*)&end && (void*)(c) <= (void*)&edata && !inStack((void*)c);
    }
}

class Test {
    public:
    const char* d;
    Test(const char* c) { d = c; }
};

static const char* str5 = "short";

int main() {
    const char* str1 = "short";
    char* str2 = const_cast<char*>("short");
    const char str3[6] = {'s', 'h', 'o', 'r', 't', 0};
    const char* str4 = "longgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg";
    std::cout << roMem((void*)str1) << std::endl; // prints 1
    std::cout << roMem((void*)(const char*)str2) << std::endl; // prints 1
    std::cout << roMem((void*)str3) << std::endl; // prints 0
    std::cout << roMem((void*)str4) << std::endl; // prints 1
    std::cout << roMem((void*)str5) << std::endl; // prints 1
    std::cout << roMem((void*)"short") << std::endl; // prints 1
    std::cout << roMem((void*)(const char*)2) << std::endl; // prints 0
    Test x((const char*)3);
    std::cout << roMem((void*)x.d) << std::endl; // prints 0
}


在我的Ubuntu 22.04机器上,上面的似乎工作正常(使用g12和linker ld或clang13编译),但问题是,(following this),它似乎也是在代码文本段之后立即生成传统的数据段序列-在这种情况下,老式的(ptr > &etext) && (ptr < &edata);就足够了。问题是当机器不遵循传统的数据段紧接着文本段的顺序时。

u4dcyp6a

u4dcyp6a1#

/proc/self/maps。沿着:

#include <cstdint>
#include <fstream>
#include <iomanip>
#include <ios>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <vector>

std::optional<bool> roMem(uintptr_t addr) {
    auto f = std::ifstream("/proc/self/maps");
    while (f) {
        uintptr_t start;
        uintptr_t stop;
        char c;
        if ((f >> std::hex >> start >> c >> stop >> c >> c) && start <= addr && addr < stop) {
            return c != 'w';
        }
        f.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
    return {};
}

template <typename T> std::optional<bool> roMem(T* c) {
    return roMem(reinterpret_cast<uintptr_t>(c));
}

std::ostream& operator<<(std::ostream& ss, std::optional<bool> o) {
    return ss << (o ? *o ? "true" : "false" : "none");
}

static const char* str5 = "short";
int main() {
    std::cout << roMem(str5) << '\n';
}

字符串

相关问题