发送参数的地址是否安全?

Is it safe to send an address of a parameter?

考虑以下代码片段:

void read_write(unsigned* ptr, bool r_or_w) {
    if (r_or_w) {
        *ptr = read_from_device_register();
    } else {
        write_to_device_register(*ptr);
    }
}

unsigned read(void) {
    unsigned data = 0;
    read_write(&data, 1);
    return data;
}

void write(unsigned data) {
    read_write(&data, 0); // <--- sending address of received argument
}

具体来说,请考虑以下事实:void write(unsigned data) 发送指向它作为参数接收的值的指针。

比较:

void write(unsigned w_data) {
    unsigned data = w_data;
    read_write(&data, 0); // <--- sending address of local variable
}

在这两种情况下 data 都存储在堆栈中,只有在第一种情况下 data 存储在调用者的调用框架中,而不是作为局部变量存储在被调用者中.安全吗?有什么陷阱吗?如果有的话,有多常见?

此处的生命周期没有问题,只要 read_write 不将指针存储在可以比 write 框架中的指针 data 更长寿的地方。因此,从程序正确性的角度来看,这是安全的。事实上,给定的两个选项之间没有区别——可以像获取局部变量的地址一样获取传入参数的地址。至于在 write 的调用者的框架中修改变量的问题,这也是无关紧要的 - data 是按值传递的。

但是,如果函数经常被调用或在紧密循环中调用,则无符号整数的指针间接寻址可能会产生很大的开销。

此外,从设计的角度来看,这似乎是一个笨拙的设计,有点难以阅读; write 委托给 read_write,最终委托给 write_to_device_register。这在直觉上有点笨拙,并且不必要地将读取和写入路径混合在一起只是为了让它们再次发散;这可能是不安全的,因为与更简单的设计相比,代码更脆弱,更容易维护错误。

语言规范没有说明参数存储在哪里;您所描述的关于哪个堆栈帧保存地址的内容是一个不属于该语言的实现细节。 C 甚至没有指定有一个堆栈,尽管它是典型的实现。

变量的位置与回答这个问题无关,只与变量的生命周期有关。函数参数的生命周期与函数顶层块中的局部变量相同。您可以将地址传递给另一个函数,只要它仅在 write() 返回之前使用,它就是有效的。如果地址保存在其他数据结构中,在 write() returns 之后使用它会导致未定义的行为。

因此,就您的问题而言,使用 &w_data&data 没有区别。一个好的编译器优化器甚至可能为两个版本生成相同的代码。

绝对安全。

即使 read_write(&data, 0); 使用指针更改 data 的值,该更改也不会影响 void write(unsigned data){..} 之外的任何内容。函数内部的 unsigned data 就像一个局部函数变量一样,除了它是由调用函数时使用的参数值初始化的。

这是 C 中一个简单但重要的特性:参数总是按值传递。函数不能更改原始参数的值。如果参数是一个指针,函数可以改变指向对象的值,但不能改变参数本身的值。

示例:

void f1(int* p)
{
    *p = 42;
}

void f2(int n)
{
    printf("At start of f2 n is %d\n", n);
    f1(&n);
    printf("At end of f2 n is %d\n", n);
}

void f3(int* p)
{
    printf("At start of f3 *p is %d\n", *p);
    f2(*p);               
    printf("At end of f2 *p is %d\n", *p);
}

像这样调用上面的链

int x = 17;
printf("Before f3 x is %d\n", x);
f3(&x);                          
printf("After f3 x is %d\n", x);

将产生(评论是我的):

Before f3 x is 17
At start of f3 *p is 17
At start of f2 n is 17
At end of f2 n is 42     // notice: f1 changed the value of n inside f2
At end of f3 *p is 17    // but when f2 returns the value of the original x is the same
After f3 x is 17         // Reason: x and n are to different variables

only that in the first case data is stored on the call-frame of the caller, rather than in the callee as a local variable.

实际上不,按照您声明的方式 w_data,它将按值复制到函数中。在 write() 执行时,它“拥有”w_data,因为 w_data 是其范围的一部分。事实上,对于大多数 ABI,函数的前 3 或 4 个参数是在硬件寄存器中传递的,而不是在堆栈中传递的。为了将内存位置传递给 read_write(),编译器必须首先在函数的堆栈帧中创建一个条目,将寄存器的值存储在该条目中,并将该条目的地址传递给 read_write().

Is it safe? are there any pitfalls?

是的,它很安全。将地址传递给自动变量总是存在陷阱,但这些都包含在对值的范围及其将如何使用的完整理解中。例如,假设 read_write() 函数是使用“device_register”的 DMA 实现的。这可能是一个异步调用,在这种情况下,w_data 变量会在操作完成之前超出范围。效果是不确定的。

How common is it, if at all?

在低级软件应用程序中极为常见。