为什么保存受限指针的值不会产生未定义的行为?
Why is saving a restricted pointer's value not giving undefined behavior?
我正在对虚拟机进行编程,我创建了一个受限指针的联合来迭代脚本的指令流:
union Pointer {
uint8_t *restrict UInt8Ptr;
uint16_t *restrict UInt16Ptr;
uint32_t *restrict UInt32Ptr;
uint64_t *restrict UInt64Ptr;
int8_t *restrict Int8Ptr;
int16_t *restrict Int16Ptr;
int32_t *restrict Int32Ptr;
int64_t *restrict Int64Ptr;
float *restrict FloatPtr;
double *restrict DoublePtr;
const char *restrict CStrPtr;
void *restrict Ptr;
};
对于 CALL 操作码,我(间接)保存了指令指针的值,如果我理解 "restrict" 关键字的用法,这将导致未定义的行为。
(--regs[regStk].SelfPtr)->Ptr = ip.Ptr; /* push rip */
*--regs[regStk].SelfPtr = regs[regBase]; /* push rbp */
regs[regBase] = regs[regStk]; /* mov rbp, rsp */
我还应该说,在 RET 操作码中,指令指针的值被恢复。
regs[regStk] = regs[regBase]; /* mov rsp, rbp */
regs[regBase] = *regs[regStk].SelfPtr++; /* pop rbp */
ip.Ptr = (*regs[regStk].SelfPtr++).Ptr; /* pop rip */
我做了很多很多测试,甚至使用了不同的编译器(GCC 和 clang v3.5 和 clang v6.0),这似乎没有产生未定义的行为,这是为什么呢?
编辑更新:
变量都声明为局部块作用域:
int32_t VM_Exec(struct VM *const restrict vm)
{
if( !vm or !vm->CurrScript.Ptr ) {
return ErrInstrBounds;
}
union Value *const restrict regs = vm->Regs; // <--
union Pointer pc = (union Pointer){.UInt8Ptr = regs[regInstr].UCharPtr}; // <--
restrict 关键字仅在更高级别的优化时才被考虑。仅限 gcc -O2 和 -O3。
在您的示例中,我没有看到任何可能导致问题的原因,因为我们不知道这些数组的声明方式和使用方式。
这里有一个例子——我违背了与编译器的约定。
unsigned p = 100;
void foo1(void)
{
p++;
}
void foo(unsigned *restrict x)
{
printf("p = %u\n", *x);
foo1();
printf("p = %u\n", *x);
}
int main()
{
foo(&p);
}
结果是 (-O3)
100
100
使用 -O1 编译
100
101
另一个例子:-O3
unsigned p = 100;
void foo1(void)
{
p++;
}
void foo(unsigned *restrict x)
{
unsigned *restrict p1;
p1 = x;
printf("p = %u\n", *x);
foo1();
printf("p = %u\n", *x);
*p1++;
printf("p = %u\n", *x);
}
int main()
{
foo(&p);
}
结果:
p = 100
p = 100
p = 101
restrict
限定符的目的是说,相对于某个代码区域内的任何其他操作,对某些指针的操作可以自由地重新排序。虽然标准允许将限定符与生命周期与任何特定代码区域无关的指针对象关联使用,但标准只允许将指针值 P 存储到此类对象中,前提是对于每个存储字节将永远通过派生自 P 的指针访问 以下情况之一为真:
字节是静态对象的一部分,其值在程序执行期间永远不会改变。
有问题的对象从来没有,也永远不会通过任何不是派生自 P 的指针访问。
这样的限制非常严格,以至于很少有情况 restrict
限定符可以明智地用于除自动对象之外的任何东西,而且我不知道有任何编译器试图做任何事情在其他情况下与此类限定符一起使用。
我正在对虚拟机进行编程,我创建了一个受限指针的联合来迭代脚本的指令流:
union Pointer {
uint8_t *restrict UInt8Ptr;
uint16_t *restrict UInt16Ptr;
uint32_t *restrict UInt32Ptr;
uint64_t *restrict UInt64Ptr;
int8_t *restrict Int8Ptr;
int16_t *restrict Int16Ptr;
int32_t *restrict Int32Ptr;
int64_t *restrict Int64Ptr;
float *restrict FloatPtr;
double *restrict DoublePtr;
const char *restrict CStrPtr;
void *restrict Ptr;
};
对于 CALL 操作码,我(间接)保存了指令指针的值,如果我理解 "restrict" 关键字的用法,这将导致未定义的行为。
(--regs[regStk].SelfPtr)->Ptr = ip.Ptr; /* push rip */
*--regs[regStk].SelfPtr = regs[regBase]; /* push rbp */
regs[regBase] = regs[regStk]; /* mov rbp, rsp */
我还应该说,在 RET 操作码中,指令指针的值被恢复。
regs[regStk] = regs[regBase]; /* mov rsp, rbp */
regs[regBase] = *regs[regStk].SelfPtr++; /* pop rbp */
ip.Ptr = (*regs[regStk].SelfPtr++).Ptr; /* pop rip */
我做了很多很多测试,甚至使用了不同的编译器(GCC 和 clang v3.5 和 clang v6.0),这似乎没有产生未定义的行为,这是为什么呢?
编辑更新:
变量都声明为局部块作用域:
int32_t VM_Exec(struct VM *const restrict vm)
{
if( !vm or !vm->CurrScript.Ptr ) {
return ErrInstrBounds;
}
union Value *const restrict regs = vm->Regs; // <--
union Pointer pc = (union Pointer){.UInt8Ptr = regs[regInstr].UCharPtr}; // <--
restrict 关键字仅在更高级别的优化时才被考虑。仅限 gcc -O2 和 -O3。
在您的示例中,我没有看到任何可能导致问题的原因,因为我们不知道这些数组的声明方式和使用方式。
这里有一个例子——我违背了与编译器的约定。
unsigned p = 100;
void foo1(void)
{
p++;
}
void foo(unsigned *restrict x)
{
printf("p = %u\n", *x);
foo1();
printf("p = %u\n", *x);
}
int main()
{
foo(&p);
}
结果是 (-O3)
100
100
使用 -O1 编译
100
101
另一个例子:-O3
unsigned p = 100;
void foo1(void)
{
p++;
}
void foo(unsigned *restrict x)
{
unsigned *restrict p1;
p1 = x;
printf("p = %u\n", *x);
foo1();
printf("p = %u\n", *x);
*p1++;
printf("p = %u\n", *x);
}
int main()
{
foo(&p);
}
结果:
p = 100
p = 100
p = 101
restrict
限定符的目的是说,相对于某个代码区域内的任何其他操作,对某些指针的操作可以自由地重新排序。虽然标准允许将限定符与生命周期与任何特定代码区域无关的指针对象关联使用,但标准只允许将指针值 P 存储到此类对象中,前提是对于每个存储字节将永远通过派生自 P 的指针访问 以下情况之一为真:
字节是静态对象的一部分,其值在程序执行期间永远不会改变。
有问题的对象从来没有,也永远不会通过任何不是派生自 P 的指针访问。
这样的限制非常严格,以至于很少有情况 restrict
限定符可以明智地用于除自动对象之外的任何东西,而且我不知道有任何编译器试图做任何事情在其他情况下与此类限定符一起使用。