0

用 C 编码时的一个常见情况是编写返回指针的函数。如果在运行时编写的函数中发生错误,NULL可能会返回以指示错误。NULL只是特殊的内存地址 0x0,它从不用于任何事情,只是表示特殊情况的发生。

我的问题是,是否有任何其他特殊的内存地址永远不会用于用户级应用程序数据?

我想知道这一点的原因是因为它可以有效地用于错误处理。考虑一下:

#include <stdlib.h>
#include <stdio.h>

#define ERROR_NULL 0x0
#define ERROR_ZERO 0x1

int *example(int *a) {
    if (*a < 0)
        return ERROR_NULL;
    if (*a == 0)
        return (void *) ERROR_ZERO;
    return a;
}

int main(int argc, char **argv) {
    if (argc != 2) return -1;
    int *result;
    int a = atoi(argv[1]);
    switch ((int) (result = example(&a))) {
        case ERROR_NULL:
            printf("Below zero!\n");
            break;

        case ERROR_ZERO:
            printf("Is zero!\n");
            break;

        default:
            printf("Is %d!\n", *result);
            break;
    }
    return 0;
}

知道一些用户级应用程序永远不会使用的特殊地址范围可以有效地用于更有效和更清洁的条件处理。如果您知道这一点,它适用于哪些平台?

我猜跨度将是操作系统特定的。我对 Linux 最感兴趣,但也很高兴了解 OS X、Windows、Android 和其他系统。

4

8 回答 8

5

NULL 只是特殊的内存地址 0x0,它从不用于任何事情,只是表示特殊情况的发生。

这并不完全正确:有些计算机NULL内部的指针不是零(链接)。

是否有任何其他特殊的内存地址永远不会用于用户级应用程序?

EvenNULL不是普遍的;考虑到用 C 语言编程的不同平台的数量,没有其他普遍未使用的内存地址,这并不奇怪。

但是,没有人阻止您在内存中定义自己的特殊地址,将其设置为全局变量,并将其视为错误指示器。这将适用于所有平台,并且不需要特殊的地址位置。

在标题中:

extern void* ERROR_ADDRESS;

在 C 文件中:

static int UNUSED;
void *ERROR_ADDRESS = &UNUSED;

此时,ERROR_ADDRESS指向一个全局唯一位置(即 的位置UNUSED,它是定义它的编译单元的本地位置),您可以使用它来测试指针是否相等。

于 2013-03-06T00:07:28.897 回答
1

它完全取决于计算机和操作系统。例如,在像 Game Boy Advance 这样具有内存映射 IO 的计算机上,您可能不想将“左上角像素是什么颜色”的地址与用户空间数据混淆:

http://www.coranac.com/tonc/text/hardware.htm#sec-memory

于 2013-03-06T00:08:52.773 回答
1

作为程序员,你不应该担心地址,因为它在不同的平台上是不同的,在实际的硬件地址和你的应用程序之间你有很多层。在大多数现代操作系统上,物理到虚拟的转换是其中一项重要的转换,虚拟地址空间被映射到内存中,每个进程都有自己的地址空间,在硬件级别受到其他进程的保护。

您在此处指定的只是十六进制值,它们不会被解释为地址。设置为 NULL 的指针本质上是说它不指向任何东西,甚至不指向地址零。它只是空。无论它的价值是什么,都取决于平台、编译器和许多其他东西。

未定义将指针设置为任何其他值。指针是一个存储另一个地址的变量,你要做的是给这个指针一些其他的值而不是有效的值。

于 2013-03-06T00:09:58.760 回答
1

答案很大程度上取决于您的 C 编译器以及您的 CPU 和操作系统,您编译的 C 程序将在其中运行。

您的用户级应用程序通常永远无法通过指向 OS 内核数据和代码的指针访问数据或代码。并且操作系统通常不会将此类指针返回给应用程序。

通常,它们也永远不会获得指向没有物理内存支持的位置的指针。您只能通过错误(代码错误)或有目的地构造这样的指针来获得这样的指针。

C 标准无论如何都没有定义指针的有效范围是什么,什么不是。在 C 中,有效指针是NULL指向生命周期尚未结束的对象的指针或指针,它们可以是全局变量和局部变量,也可以是在malloc()'d内存和函数中创建的变量。操作系统可以通过返回来扩展这个范围:

  • 指向未在 C 程序的源代码级别明确定义的代码或数据对象的指针(操作系统可能允许应用程序直接访问其某些代码或数据,但这并不常见,或者操作系统可能允许应用程序访问它们的某些部分要么在应用加载时由操作系统创建,要么在编译应用时由编译器创建,一个例子是 Windows 让应用检查它们的可执行 PE 映像,你可以询问 Windows 映像在内存中的哪里开始)
  • 指向操作系统为/代表应用程序分配的数据缓冲区的指针(在这里,通常,操作系统将使用它自己的 API 而不是您的应用程序的malloc()/ free(),并且您需要使用适当的操作系统特定函数来释放此内存)
  • 无法取消引用且仅用作错误指示符的特定于操作系统的指针(例如,您可能拥有多个可引用的指针,例如NULL,您ERROR_ZERO是可能的候选者)

我通常不鼓励在程序中使用硬编码和魔术指针。

如果由于某种原因,指针是传达错误条件的唯一方法并且其中有多个错误条件,则可以这样做:

char ErrorVars[5] = { 0 };
void* ErrorPointer1 = &ErrorVars[0];
void* ErrorPointer2 = &ErrorVars[1];
...
void* ErrorPointer5 = &ErrorVars[4];

然后,您可以在不同的错误条件下返回,然后将返回的值与它们进行比较ErrorPointer1ErrorPointer1不过,这里有一个警告。您不能使用>, >=, <,合法地将返回的指针与任意指针进行比较<=。只有当两个指针都指向或指向同一个对象时,这才是合法的。因此,如果您想要这样的快速检查:

if ((char*)(p = myFunction()) >= (char*)ErrorPointer1 &&
    (char*)p <= (char*)ErrorPointer5)
{
  // handle the error
}
else
{
  // success, do something else
}

p只有当等于这 5 个错误指针之一时它才是合法的。如果不是,您的程序可以合法地以任何可以想象和无法想象的方式运行(这是因为 C 标准是这样说的)。为避免这种情况,您必须单独将指针与每个错误指针进行比较:

if ((p = myFunction()) == ErrorPointer1)
  HandleError1();
else if (p == ErrorPointer2)
  HandleError2();
else if (p == ErrorPointer3)
  HandleError3();
...
else if (p == ErrorPointer5)
  HandleError5();
else
  DoSomethingElse();

同样,指针是什么以及它的表示是什么,是特定于编译器和 OS/CPU 的。C 标准本身不要求有效和无效指针的任何特定表示或范围,只要这些指针按照 C 标准的规定运行(例如,指针算术与它们一起工作)。关于这个话题有一个很好的问题

因此,如果您的目标是编写可移植的 C 代码,请不要使用硬编码和“魔术”指针,而更喜欢使用其他东西来传达错误条件。

于 2013-03-06T03:22:11.920 回答
0

这段代码:

#define ERROR_NULL 0x0
#define ERROR_ZERO 0x1

int *example(int *a) {
    if (*a < 0)
        return ERROR_NULL;
    if (*a == 0)
        return (void *) ERROR_ZERO;
    return a;
}

定义了一个函数,该函数example接受输入参数a并将输出作为指向 的指针返回int。同时,当错误发生时,该函数滥用强制转换void*来将错误代码返回给调用者,就像它返回正确的输出数据一样。这种方法是错误的,因为调用者必须知道有时会收到有效的输出,但它实际上并不包含所需的输出,而是包含错误代码

是否还有其他永远不会使用的特殊内存地址...?
...它可以有效地用于错误处理

不要对可能返回的地址做任何假设。当您需要将返回码传递给调用者时,您应该以更直接的方式进行。您可以将指向输出数据的指针作为参数并返回标识成功或失败的错误代码:

#define SUCCESS     0x0
#define ERROR_NULL  0x1
#define ERROR_ZERO  0x2

int example(int *a, int** out) {
    if (...)
        return ERROR_NULL;
    if (...)
        return ERROR_ZERO;
    *out = a;
    return SUCCESS;
}
...
int* out = NULL;
int retVal = example(..., &out);
if (retVal != SUCCESS)
    ...
于 2013-03-06T00:10:27.343 回答
0

实际上 NULL(0) 是一个有效的地址。但这不是您通常可以写入的地址。

从内存中,NULL 可能是一些旧的 VAX 硬件与一些非常旧的 c 编译器不同的值。也许有人可以证实这一点。正如 C 标准定义的那样,它现在总是 0 - 请参阅这个问题Is NULL always false?

通常,从函数返回错误的方式是设置 errno。如果错误代码在特定情况下有意义,您可以背负这一点。但是,如果您需要自己的错误,则可以执行与 errno 方法相同的操作。

就个人而言,我更喜欢不返回 void* 而是让函数采用 void** 并在那里返回结果。然后您可以直接返回错误代码,其中 0 = 成功。

例如

int posix_memalign(void **memptr, size_t alignment, size_t size);

注意分配的内存在 mempr 中返回。结果代码由函数调用返回。不像malloc。

void *malloc(size_t size)
于 2013-03-06T00:12:16.817 回答
0

在 Linux 上,在 64 位和使用 x86_64 架构(来自 Intel 或 AMD)时,仅使用 64 位总地址空间的 48 位(硬件限制 AFAIK)。基本上,现在可以使用2 47到 2 62之后的任何地址,因为它不会被分配。

在某些背景下,Linux 进程的虚拟地址空间由用户空间和内核空间组成。在上述架构中,前 47 位 (128 TB) 用于用户空间。内核空间用于频谱的末端,因此最后 128 TB 位于完整 64 位地址空间的末端。介于两者之间的是无名之地。尽管这可能会在未来的任何时候改变,而且这不是便携式的。

但是我可以想到许多其他方法来返回错误而不是您的方法,所以我看不到使用这种hack的优势。

于 2013-03-06T10:59:02.663 回答
0

TL;博士:

  • -1如果您只想在 NULL 旁边再添加一个错误条件,请使用

  • 对于更特殊的情况,只需设置最低有效位,因为来自malloc()家庭的返回值或new保证与任何基本对齐方式对齐,并且低位始终为零,因此它们可以免费使用(例如在标记指针

    如果分配成功,则返回一个指针,该指针适用于任何具有基本对齐的对象类型。

    https://en.cppreference.com/w/c/memory/malloc

    指向比 char 更宽的类型的指针也总是对齐的。如果您指向堆栈上的 char 或 char 数组,则只需根据需要对齐alignas

  • 对于更多条件,您可以限制分配地址的范围。这需要特定于平台的代码,并且不会有可移植的解决方案


正如其他人所说,这在很大程度上取决于。但是,如果您在具有动态分配的平台上,那么-1(极有可能)是一个安全值

这是因为内存分配器以BIG BLOCKS的形式分配内存,而不仅仅是单个字节§。因此,可以返回的最后一个地址是-block_size。例如,如果block_size是 4,那么最后一个块将跨越地址 { -4, -3, -2, -1 },最后一个可能的地址将是 -4 = 0xFFFF...FFFC。结果,家庭永远不会返回-1malloc()

Linux 上的各种系统函数也会为无效指针返回 -1而不是 NULL,例如mmap()and shmat()。返回句柄的 Win32 API 也可以为失败情况或格式错误的句柄返回 NULL (0) 或 INVALID_HANDLE_VALUE (-1)。他们必须这样做,因为有时NULL是一个有效的内存地址。事实上,如果您使用的是哈佛架构,那么数据空间中的零位置非常有用。甚至在冯诺依曼架构上,你所说的

“NULL 只是特殊的内存地址 0x0,它从不用于任何事情,只是表示发生了特殊情况”

仍然是错误的,因为地址 0 也是有效的。只是大多数现代操作系统以某种方式映射页面零以使其在用户空间代码取消引用它时陷入困境。然而,该页面可以从内核代码中访问。Linux内核中存在一些与NULL指针取消引用错误相关的漏洞利用

事实上,与零页最初的优先使用完全相反,一些现代操作系统,如 FreeBSD、Linux 和 Microsoft Windows,实际上使零页无法访问以捕获对 NULL 指针的使用。这很有用,因为 NULL 指针是用于表示不指向任何内容的引用值的方法

https://en.wikipedia.org/wiki/Zero_page

在 MSVC 和 GCC 中,指向成员的 NULL 指针也表示为 32 位机器上的位模式 0xFFFFFFFF。并且在 AMD GCN NULL 指针中也有 -1 的值


通过利用指针通常对齐的事实,您可以更进一步并返回更多错误代码。例如,malloc总是“对齐适合任何对象类型的内存(实际上,这意味着它对齐到alignof(max_align_t))

如今,默认对齐方式malloc是 8 或 16 字节,具体取决于您使用的是 32 位还是 64 位操作系统,这意味着您将至少有 3 位可用于错误报告或您的任何目的。如果你使用指向比 char 更宽的类型的指针,那么它总是对齐的。所以通常没有什么好担心的,除非你想返回一个不是输出的 char 指针malloc(在这种情况下你可以很容易地对齐)。只需检查最低有效位以查看它是否是有效指针

int* result = func();
if ((uintptr_t)result & 1)
    error_happened(); // now the high bits can be examined to check the error condition

在 16 字节对齐的情况下,有效地址的最后 4 位始终为 0,有效地址的总数仅为位模式总数的 ¹⁄₁₆,这意味着您最多可以返回 ¹⁵⁄₁₆× 2 64 个错误代码,带有 64 位指针。然后是aligned_alloc如果你想要更多的最低有效位。

该技巧已用于在指针本身中存储一些信息。在许多 64 位平台上,您还可以使用高位来存储更多数据。请参阅在 64 位指针中使用额外的 16 位


在操作系统的帮助下,您甚至可以通过限制分配指针的范围来达到极端。例如,如果您指定必须在 2-3GB 范围内分配指针,那么任何低于 2GB 和高于 3GB 的地址都可供您使用以指示错误情况。关于如何做到这一点,请参阅:

也可以看看


§这很明显,因为需要存储有关分配块的一些信息以进行记账,因此块大小必须比块本身大得多,否则元数据本身将比 RAM 量更大。因此,如果您打电话malloc(1),那么它仍然必须为您保留一个完整的块。

于 2019-01-28T15:39:29.557 回答