指针地址跨越各种平台

时间:2013-03-06 00:03:15

标签: c pointers error-handling memory-address void-pointers

在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和其他系统也很有用。

8 个答案:

答案 0 :(得分:5)

  

NULL只是特殊的内存地址0x0,除了表示特殊条件的出现之外,它从不用于任何内容。

这不完全正确:有些NULL指针在内部不是零(link)。

  

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

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

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

在标题中:

extern void* ERROR_ADDRESS;

在C档案中:

static int UNUSED;
void *ERROR_ADDRESS = &UNUSED;

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

答案 1 :(得分:1)

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

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

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

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

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

通常,他们也永远不会获得指向未被物理内存备份的位置的指针。你只能通过错误(代码错误)或有目的地构造这样的指针来获得这样的指针。

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

  • 指向未在C程序源代码级别明确定义的代码或数据对象的指针(操作系统可能允许应用程序直接访问其部分代码或数据,但这种情况并不常见,或者操作系统可能会让应用程序访问某些代码或数据应用程序加载时由操作系统创建的部件或编译应用程序时由编译器创建的部件,例如Windows允许应用程序检查其可执行PE图像,您可以向Windows询问图像在内存中的起始位置< / LI>
  • 指向操作系统为/代表应用程序分配的数据缓冲区的指针(通常,操作系统会使用自己的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标准规定的一样(例如,指针算法与它们一起工作)。有good question on the topic

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

答案 4 :(得分: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)
    ...

答案 5 :(得分:0)

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

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

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

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

e.g。

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

注意分配的内存在memptr中返回。函数调用返回结果代码。与malloc不同。

void *malloc(size_t size)

答案 6 :(得分:0)

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

对于某些背景,Linux进程的虚拟地址空间由用户和内核空间组成。在上述架构中,第一个47位(128 TB)用于用户空间。内核空间用于频谱的末尾,因此在完整的64位地址空间的末尾使用最后的128 TB。其间是 terra incognita 。虽然这可能会在未来的任何时间发生变化,但这不是便携式的。

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

答案 7 :(得分:0)

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

那是因为内存分配器以大块而不是单个字节的形式发出了内存§。因此,可以返回的最后一个地址为-block_size。例如,如果block_size为4,则最后一个块将跨越地址{-4,-3,-2,-1},并且最后一个可能的地址将为-4 = 0xFFFF ... FFFC。结果, -1不会由malloc()系列返回

Linux上的各种系统函数也会为无效指针(而不是NULL)返回-1。例如mmap()shmat()。自sometimes NULL is a valid memory address起,他们必须这样做。实际上,如果您使用的是哈佛体系结构,那么在数据空间中零位置是非常有用的。甚至在冯·诺依曼(Von Neumann)架构上,您所说的话

  

“ NULL只是特殊的内存地址0x0,除了用于指示特殊情况的发生以外,它永远不会用于任何其他用途”

仍然是错误的,因为地址0也有效。只是大多数现代OS都以某种方式将页面零映射到用户空间代码取消引用页面时使其陷入陷阱。但是该页面仍可从内核代码中访问。 Linux内核中有一些与NULL pointer dereference bug相关的漏洞

  

实际上,与零页最初的优先使用完全相反,某些现代操作系统(例如FreeBSD,Linux和Microsoft Windows)实际上使零页无法访问以捕获NULL指针的使用。这很有用,因为NULL指针是用于表示指向什么都没有的引用的值的方法

     

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

在MSVC中,指向成员的NULL指针也是represented as the bit pattern 0xFFFFFFFF on a 32-bit machine


您可以利用指针通常对齐的事实,走得更远并返回更多错误代码。例如,malloc总是"aligns memory suitable for any object type (which, in practice, means that it is aligned to alignof(max_align_t))"

如今,malloc的默认对齐方式是8字节或16字节,具体取决于您使用的是32位还是64位操作系统,这意味着您将至少有3位可用于错误报告。而且,如果您使用的指针指向的类型比char宽,则它会始终对齐。因此,通常没有什么可担心的,除非您想返回一个不是从malloc输出的char指针。只需检查最低有效位,看看它是否是有效指针

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,有效地址的总数仅是位模式总数的1/3,这意味着您最多可以返回¹带有64位指针的⁄₁₆×2 64 错误代码。如果您想要更多最低有效位,则使用aligned_alloc

该技巧已用于在指针本身中存储一些信息。在许多64位平台上,您也可以使用高位来存储更多数据。参见Using the extra 16 bits in 64-bit pointers

另请参见


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