一项模糊行为的研究

本文探讨了非空函数完成而未调用适当值的return时在c ++中发生的未定义行为的可能表现。这篇文章比实际更具科学性和娱乐性。

谁不喜欢跳耙的乐趣-我们路过,我们不会停下来。

介绍


每个人都知道,在开发C ++代码时,不应允许未定义的行为。
然而:

  • 由于可能后果的抽象性,不确定行为似乎不够危险;
  • 不一定总是在哪里。

让我们尝试指定在一种相当简单的情况下发生的未定义行为的可能表现-在非void函数中,没有返回值。

为此,请考虑由最流行的编译器在不同的优化模式下生成的代码。

Linux下的研究将使用Compiler Explorer进行研究Windows和macOs X-在直接提供给我的硬件上。

所有构建都将针对x86-x64完成。

不会采取任何措施来增强或抑制编译器警告/错误。

会有很多反汇编的代码。不幸的是,它的设计是杂色的,因为 我必须使用几种不同的工具(嗯,至少我设法在各处获得了Intel语法)。我将对反汇编代码给出适当详细的注释,但是,这些注释并不能消除对处理器寄存器和堆栈原理的了解。

阅读标准


C ++ 11最终草案n3797,C ++ 14最终草案N3936:
6.6.3 return语句
...
从函数末尾流出等同于没有值的return;这导致
返回值函数中的行为不确定
...

到达函数的末尾等效于没有返回值的返回。对于提供返回值的函数,这将导致未定义的行为。

C ++ 17草案N4713
9.6.3 return语句
...
离开构造函数,析构函数或具有cv void返回类型的函数的末尾等效于没有操作数的返回。否则,从main(6.8.3.1)以外的函数的末尾流出会导致未定义的行为。
...

如果返回的构造函数,析构函数或函数的返回值为空(可能带有const和volatile限定符),则等于没有返回值的return。对于所有其他功能,这将导致未定义的行为(主功能除外)。

实际上这是什么意思?

如果函数签名提供了返回值:

  • 它的执行应以带有适当类型实例的return语句结尾;
  • 否则,行为含糊;
  • 未定义行为不是从调用函数的那一刻开始的,不是从使用返回值的那一刻开始的,而是从函数未正确完成的那一刻开始;
  • 如果函数同时包含正确和错误的执行路径-未定义的行为只会在错误的路径上发生;
  • 问题中未定义的行为不会影响函数主体中包含的指令的执行。

关于主要功能的短语在c ++ 17中并不陌生-在标准的早期版本中,第3.6.1节“主要功能”中描述了类似的例外情况。

示例1-布尔


在c ++中,没有类型的状态比布尔更简单。让我们从他开始。

#include <iostream>

bool bad() {};

int main()
{
    std::cout << bad();

    return 0;
}

对于这样的示例,MSVC会生成C4716编译错误,因此,通过提供至少一个正确的执行路径,MSVC的代码将不得不稍微复杂一些:

#include <iostream>
#include <stdlib.h>

bool bad()
{
    if (rand() == 0) {
        return true;
    }
}

int main()
{
    std::cout << bad();

    return 0;
}

汇编:

平台编译器编译结果
的Linuxx86-x64铛10.0.0警告:非无效函数不会返回值[-Wreturn-type]
的Linuxx86-x64 gcc 9.3警告:函数中没有return语句,返回非空[-Wreturn-type]
macOS XApple clang版本11.0.0警告:控制到达非无效函数的结尾[-Wreturn-type]
视窗MSVC 2019版本16.5.4原始示例为错误C4716,复杂-警告C4715:并非所有控制路径都返回值

执行结果:
优化程序返回控制台输出
Linux x86-x64 Clang 10.0.0
-O0255无输出
-O1,-O20无输出
Linux x86-x64 gcc 9.3
-O0089
-O1,-O2,-O30无输出
macOs X Apple clang版本11.0.0
-O0,-O1,-O200
Windows MSVC 2019 16.5.4,原始示例
/ Od,/ O1,/ O2没有构建没有构建
Windows MSVC 2019 16.5.4复杂的示例
/ od041
/ O1,/ O201个

即使在这个最简单的示例中,四个编译器也演示了至少三种显示未定义行为的方式。

让我们弄清楚这些编译器在那里编译了什么。

Linux x86-x64 Clang 10.0.0,-O0


图片

bad()函数中的最后一条语句是ud2英特尔64和IA-32架构软件开发人员手册中

的指令说明
UD2—Undefined Instruction
Generates an invalid opcode exception. This instruction is provided for software testing to explicitly generate an invalid opcode exception. The opcode for this instruction is reserved for this purpose.
Other than raising the invalid opcode exception, this instruction has no effect on processor state or memory.

Even though it is the execution of the UD2 instruction that causes the invalid opcode exception, the instruction pointer saved by delivery of the exception references the UD2 instruction (and not the following instruction).

This instruction’s operation is the same in non-64-bit modes and 64-bit mode.

简而言之,这是引发异常的特殊指令。

您需要将bad()调用包装在try ... catch中!

不管怎样。这不是c ++异常。

是否可以在运行时捕获ud2?
在Windows上,应使用__try;在Linux和macOs X上,应使用SIGILL信号处理程序。

Linux x86-x64 Clang 10.0.0,-O1,-O2


图片

作为优化的结果,编译器只需拿走并丢弃bad()函数的主体及其调用。

Linux x86-x64 gcc 9.3,-O0


图片

解释(相反的顺序,因为在这种情况下,从头到尾比较容易解析链):

5.调用bool流中的输出运算符(第14行);

4.地址std :: cout放置在edi寄存器中-这是输出运算符在流中的第一个参数(第13行);

3. eax寄存器的内容放在esi寄存器中-这是流中输出运算符的第二个参数(第12行);

2. eax的三个高字节被重置为零,al的值不变(第11行);

1.调用bad()函数(第10行);

0. bad()函数应将返回值放在al寄存器中。

取而代之的是,第4行显示nop(无操作,虚拟)。

来自al寄存器的一字节垃圾被输出到控制台。程序正常结束。

Linux x86-x64 gcc 9.3,-O1,-O2,-O3


图片

编译器将所有内容作为优化的结果。

macOs X Apple clang版本11.0.0,-O0


函数main():

图片

输出运算符的布尔参数到流的路径(这一次是直接顺序):

1. al寄存器的内容放在edx寄存器中(第8行);

2. edx寄存器的所有位都清零,除了最低位(第9行);

3.指向std :: cout的指针放置在rdi寄存器中-这是流中输出运算符的第一个参数(第10行);

4. edx寄存器的内容放在esi寄存器中-这是流中输出运算符的第二个参数(第11行);

5.在bool的流中调用输出语句(第13行);

main函数期望从al寄存器中获取bad()函数的结果。

bad()函数:

图片

1.将堆栈中下一个字节中尚未分配的值放在al寄存器中(第4行);

2.除了最低有效位(第5行)外,al寄存器的所有位均被排除。

未分配堆栈中的一小部分垃圾输出到控制台。碰巧的是,在测试运行期间结果为零。

程序正常结束。

macOs X Apple clang版本11.0.0,-O1,-O2


图片

流中输出运算符的布尔参数无效(第5行)。

在优化过程中引发了bad()调用。

该程序在控制台中始终显示零,然后正常退出。

Windows MSVC 2019 16.5.4,高级示例,/ Od


图片

可以看出bad()函数应该在al寄存器中提供一个返回值。

图片

bad()函数返回的值首先被压入堆栈,然后被压入edx寄存器以输出到流。

来自al寄存器的一字节垃圾被输出到控制台(如果更精确一点,则为rand()结果的低字节)。程序正常结束。

Windows MSVC 2019 16.5.4复杂的示例,/ O1,/ O2


图片

编译器强制内联bad()调用。主功能:

  • 从[rsp + 30h]的内存中从ebx复制一个字节;
  • 如果rand()返回零,则将单位从ecx复制到ebx(第11行);
  • 将相同的值复制到dl(更确切地说是其最低有效字节)(第13行);
  • 在流中调用输出函数,该函数输出dl值(第14行)。

来自RAM的一字节垃圾(来自地址rsp + 30h)被输出到流中。

示例1的结论


下表显示了考虑反汇编程序清单的结果:
优化程序返回控制台输出原因
Linux x86-x64 Clang 10.0.0
-O0255无输出ud2
-O1,-O20无输出优化的结果是抛出了控制台输出和对bad()函数的调用
Linux x86-x64 gcc 9.3
-O0089来自寄存器al的一字节垃圾
-O1,-O2,-O30无输出优化的结果是抛出了控制台输出和对bad()函数的调用
macOs X Apple clang版本11.0.0
-O000来自RAM的一点垃圾
-O1,-O200函数调用bad()替换为零
Windows MSVC 2019 16.5.4,原始示例
/ Od,/ O1,/ O2没有构建没有构建没有构建
Windows MSVC 2019 16.5.4复杂的示例
/ od041来自寄存器al的一字节垃圾
/ O1,/ O201个来自RAM的一字节垃圾

事实证明,编译器没有演示3种,但是演示了多达6种未定义行为的变体-在考虑反汇编程序清单之前,我们无法区分其中的一些。

示例1a-管理未定义的行为


让我们尝试用未定义的行为来引导一些操作-影响bad()函数返回的值。

这只能通过输出垃圾的编译器来完成。
为此,将所需值放到编译器将其取用的位置。

Linux x86-x64 gcc 9.3,-O0


空的bad()函数不会修改寄存器al的值,因为调用代码需要它。因此,如果我们在调用bad()之前在al中放置某个值,那么我们希望看到该值是执行bad()的结果。

显然,这可以通过调用任何其他返回bool的函数来完成。但是它也可以使用返回未成字符的char的函数来完成。

完整的示例代码
#include <iostream>

bool bad() {}

bool goodTrue()
{
    return rand();
}

bool goodFalse()
{
    return !goodTrue();
}

unsigned char goodChar(unsigned char ch)
{
    return ch;
}

int main()
{
    goodTrue();
    std::cout << bad() << std::endl;

    goodChar(85);
    std::cout << bad() << std::endl;

    goodFalse();
    std::cout << bad() << std::endl;

    goodChar(240);
    std::cout << bad() << std::endl;

    return 0;
}


输出到控制台:
1
85
0
240

Windows MSVC 2019 16.5.4,/ Od


在MSVC的示例中,bad()函数返回rand()结果的低字节。

在不修改bad()函数的情况下,外部代码可以通过修改rand()的结果来影响其返回值。

完整的示例代码
#include <iostream>
#include <stdlib.h>

void control(unsigned char value)
{
    uint32_t count = 0;
    srand(0);
    while ((rand() & 0xff) != value) {
        ++count;
    }

    srand(0);
    for (uint32_t i = 0; i < count; ++i) {
        rand();
    }
}

bool bad()
{
    if (rand() == 0) {
        return true;
    }
}

int main()
{
    control(1);
    std::cout << bad() << std::endl;

    control(85);
    std::cout << bad() << std::endl;

    control(0);
    std::cout << bad() << std::endl;

    control(240);
    std::cout << bad() << std::endl;

    return 0;
}


输出到控制台:
1
85
0
240


Windows MSVC 2019 16.5.4,/ O1,/ O2


为了不影响bad()函数“返回”的值,创建一个堆栈变量就足够了。为了使其中的记录在优化期间不会被丢弃,应将其标记为易失性。
完整的示例代码
#include <iostream>
#include <stdlib.h>

bool bad()
{
  if (rand() == 0) {
    return true;
  }
}

int main()
{
  volatile unsigned char ch = 1;
  std::cout << bad() << std::endl;

  ch = 85;
  std::cout << bad() << std::endl;

  ch = 0;
  std::cout << bad() << std::endl;

  ch = 240;
  std::cout << bad() << std::endl;

  return 0;
}


输出到控制台:
1
85
0
240


macOs X Apple clang版本11.0.0,-O0


在调用bad()之前,必须在该存储单元中输入一个特定值,该值将比调用bad()时的堆栈顶部小一。

完整的示例代码
#include <iostream>

bool bad() {}

void putToStack(uint8_t value)
{
    uint8_t memory[1]{value};
}

int main()
{
    putToStack(20);
    std::cout << bad() << std::endl;

    putToStack(55);
    std::cout << bad() << std::endl;

    putToStack(0xfe);
    std::cout << bad() << std::endl;

    putToStack(11);
    std::cout << bad() << std::endl;

    return 0;
}

-O0, memory. , .

memory , — , , .

, .. , — putToStack .

输出到控制台:
0
1
0
1

似乎已经发生了:可以更改bad()函数的输出,并且仅考虑低位。

示例1a的结论


通过一个示例可以验证反汇编程序列表的正确解释。

例子1b-破烂


好吧,您想到了,控制台中将显示“ 41”而不是“ 1”……这很危险吗?

我们将检查两个提供完整字节垃圾的编译器。

Windows MSVC 2019 16.5.4,/ Od


完整的示例代码
#include <iostream>
#include <stdlib.h>
#include <set>
#include <unordered_set>

bool bad()
{
    if (rand() == 0) {
        return true;
    }
}

int main()
{
    bool badBool1 = bad();
    bool badBool2 = bad();

    std::cout << "badBool1: " << badBool1 << std::endl;
    std::cout << "badBool2: " << badBool2 << std::endl;

    if (badBool1) {
      std::cout << "if (badBool1): true" << std::endl;
    } else {
      std::cout << "if (badBool1): false" << std::endl;
    }
    if (!badBool1) {
      std::cout << "if (!badBool1): true" << std::endl;
    } else {
      std::cout << "if (!badBool1): false" << std::endl;
    }

    std::cout << "(badBool1 == true || badBool1 == false || badBool1 == badBool2): "
              << std::boolalpha << (badBool1 == true || badBool1 == false || badBool1 == badBool2)
              << std::endl;
    std::cout << "std::set<bool>{badBool1, badBool2, true, false}.size(): "
              << std::set<bool>{badBool1, badBool2, true, false}.size()
              << std::endl;
    std::cout << "std::unordered_set<bool>{badBool1, badBool2, true, false}.size(): "
              << std::unordered_set<bool>{badBool1, badBool2, true, false}.size()
              << std::endl;

    return 0;
}


输出到控制台:
badBool1:41
badBool2:35
if(badBool1):true
if(!badBool1):false
(badBool1 == true || badBool1 == false || badBool1 == badBool2):false
std :: set <bool> {badBool1,badBool2 ,true,false} .size():4
std :: unordered_set <bool> {badBool1,badBool2,true,false} .size():4

未定义的行为导致出现布尔变量,该变量至少破坏:
  • 布尔值的比较运算符;
  • 布尔值的哈希函数。


Windows MSVC 2019 16.5.4,/ O1,/ O2


完整的示例代码
#include <iostream>
#include <stdlib.h>
#include <set>
#include <unordered_set>

bool bad()
{
  if (rand() == 0) {
    return true;
  }
}

int main()
{
  volatile unsigned char ch = 213;
  bool badBool1 = bad();
  ch = 137;
  bool badBool2 = bad();

  std::cout << "badBool1: " << badBool1 << std::endl;
  std::cout << "badBool2: " << badBool2 << std::endl;

  if (badBool1) {
    std::cout << "if (badBool1): true" << std::endl;
  }
  else {
    std::cout << "if (badBool1): false" << std::endl;
  }
  if (!badBool1) {
    std::cout << "if (!badBool1): true" << std::endl;
  }
  else {
    std::cout << "if (!badBool1): false" << std::endl;
  }

  std::cout << "(badBool1 == true || badBool1 == false || badBool1 == badBool2): "
    << std::boolalpha << (badBool1 == true || badBool1 == false || badBool1 == badBool2)
    << std::endl;
  std::cout << "std::set<bool>{badBool1, badBool2, true, false}.size(): "
    << std::set<bool>{badBool1, badBool2, true, false}.size()
    << std::endl;
  std::cout << "std::unordered_set<bool>{badBool1, badBool2, true, false}.size(): "
    << std::unordered_set<bool>{badBool1, badBool2, true, false}.size()
    << std::endl;

  return 0;
}


输出到控制台:
badBool1:213
badBool2:137
if(badBool1):true
if(!badBool1):false
(badBool1 == true || badBool1 == false || badBool1 == badBool2):false
std :: set <bool> {badBool1,badBool2 ,true,false} .size():4
std :: unordered_set <bool> {badBool1,badBool2,true,false} .size():4

启用优化后,使用损坏的布尔变量的工作不会更改。

Linux x86-x64 gcc 9.3,-O0


完整的示例代码
#include <iostream>
#include <stdlib.h>
#include <set>
#include <unordered_set>

bool bad()
{
}

unsigned char goodChar(unsigned char ch)
{
  return ch;
}

int main()
{
  goodChar(213);
  bool badBool1 = bad();

  goodChar(137);
  bool badBool2 = bad();

  std::cout << "badBool1: " << badBool1 << std::endl;
  std::cout << "badBool2: " << badBool2 << std::endl;

  if (badBool1) {
    std::cout << "if (badBool1): true" << std::endl;
  }
  else {
    std::cout << "if (badBool1): false" << std::endl;
  }
  if (!badBool1) {
    std::cout << "if (!badBool1): true" << std::endl;
  }
  else {
    std::cout << "if (!badBool1): false" << std::endl;
  }

  std::cout << "(badBool1 == true || badBool1 == false || badBool1 == badBool2): "
    << std::boolalpha << (badBool1 == true || badBool1 == false || badBool1 == badBool2)
    << std::endl;
  std::cout << "std::set<bool>{badBool1, badBool2, true, false}.size(): "
    << std::set<bool>{badBool1, badBool2, true, false}.size()
    << std::endl;
  std::cout << "std::unordered_set<bool>{badBool1, badBool2, true, false}.size(): "
    << std::unordered_set<bool>{badBool1, badBool2, true, false}.size()
    << std::endl;

  return 0;
}


输出到控制台:
badBool1:213
badBool2:137
if(badBool1):true
if(!badBool1):true
(badBool1 == true || badBool1 == false || badBool1 == badBool2):false
std ::设置<bool> {badBool1,badBool2 ,true,false} .size():4
std :: unordered_set <bool> {badBool1,badBool2,true,false} .size():4


与MSVC相比,gcc还添加了not运算符的错误操作。

示例1b的结论


使用布尔值破坏基本操作可能会对高级逻辑产生严重影响。

为什么会发生?

因为某些使用布尔变量的运算是在true严格为单位的假设下实现的。

我们不会在反汇编程序中考虑此问题-事实证明,本文篇幅如此之多。

再一次,我们将用编译器的行为来阐明该表:
优化程序返回控制台输出原因使用不良结果的后果()
Linux x86-x64 Clang 10.0.0
-O0255无输出ud2
-O1,-O20无输出优化的结果是抛出了控制台输出和对bad()函数的调用
Linux x86-x64 gcc 9.3
-O0089来自寄存器al的一字节垃圾违反工作:
不;==; !=; <; >; <=; > =; std ::哈希。
-O1,-O2,-O30无输出优化的结果是抛出了控制台输出和对bad()函数的调用
macOs X Apple clang版本11.0.0
-O000来自RAM的一点垃圾
-O1,-O200函数调用bad()替换为零
Windows MSVC 2019 16.5.4,原始示例
/ Od,/ O1,/ O2没有构建没有构建没有构建
Windows MSVC 2019 16.5.4复杂的示例
/ od041来自寄存器al的一字节垃圾违反工作:
==; !=; <; >; <=; > =; std ::哈希。
/ O1,/ O201个来自RAM的一字节垃圾违反工作:
==; !=; <; >; <=; > =; std ::哈希。

四个编译器给出了7种不同的未定义行为表示。

示例2-结构


让我们来看一个更复杂的例子:

#include <iostream>
#include <stdlib.h>

struct Test
{
    Test(uint64_t v)
        : value(v)
    {
        std::cout << "Test::Test(" << v << ")" << std::endl;
    }
    ~Test()
    {
        std::cout << "Test::~Test()" << std::endl;
    }

    uint64_t value;
};

Test bad(int v)
{
    if (v == 0) {
        return {42};
    } else if (v == 1) {
        return {142};
    }
}

int main()
{
    const auto rnd = rand();
    std::cout << "rnd: " << rnd << std::endl;

    std::cout << bad(rnd).value << std::endl;

    return 0;
}

Test结构需要一个int类型的单个参数来构造。诊断消息从其构造函数和析构函数输出。bad(int)函数有两个有效的执行路径,没有一个将在单个调用中实现。

这次-首先是表格,然后是对晦涩点的反汇编程序分析。
优化Program returnConsole output
Linux x86-x64 Clang 10.0.0
-O0255rnd: 1804289383ud2
-O1, -O20rnd: 1804289383
Test::Test(142)
142
Test::~Test()
if (v == 1) . else if else.
Linux x86-x64 gcc 9.3
-O00rnd: 1804289383
4198608
Test::~Test()
nop .
value .
-O1, -O2, -O30rnd: 1804289383
Test::Test(142)
142
Test::~Test()
if (v == 1) . else if else.
macOs X Apple clang version 11.0.0
-O0The program has unexpectedly finished.rnd: 16807ud2
-O1, -O20rnd: 16807
Test::Test(142)
142
Test::~Test()
if (v == 1) . else if else.
Windows MSVC 2019 16.5.4
/Od /RTCsAccess violation reading location 0x00000000CCCCCCCCrnd: 41MSVC stack frame run-time error checking
/Od, /O1, /O20rnd:41
8791061810776
测试::〜测试()
来自地址在rax中的内存位置的垃圾

再次,我们看到许多选择:除了已知的ud2外,至少还有4种不同的行为。

使用构造函数进行编译器处理非常有趣:

  • 在某些情况下,无需调用构造函数就可以继续执行-在这种情况下,对象处于某种随机状态;
  • 在其他情况下,在执行路径上未提供构造函数调用,这很奇怪。

Linux x86-x64 Clang 10.0.0,-O1,-O2


图片

在代码中仅进行了一次比较(第14行),并且只有一个条件跳转(第15行)。编译器忽略了第二个比较和第二个条件跳转。
这导致人们怀疑不确定行为的开始时间早于标准规定的时间。

但是检查第二个条件是否不包含任何副作用,并且编译器逻辑的工作方式如下:

  • 如果第二个条件为真-您需要使用参数142调用构造函数Test;
  • 如果第二个条件不成立,则函数将退出而不返回值,这意味着未定义的行为,编译器可以执行任何操作。包括-用相同的参数调用相同的构造函数;
  • 验证是多余的;可以在不检查条件的情况下调用带有参数142的Test构造函数。

让我们看看如果第二次检查包含有副作用的情况会发生什么:

Test bad(int v)
{
    if (v == 0) {
        return {42};
    } else if (v == rand()) {
        return {142};
    }
}

完整代码
#include <iostream>
#include <stdlib.h>

struct Test
{
    Test(uint64_t v)
        : value(v)
    {
        std::cout << "Test::Test(" << v << ")" << std::endl;
    }
    ~Test()
    {
        std::cout << "Test::~Test()" << std::endl;
    }

    uint64_t value;
};

Test bad(int v)
{
    if (v == 0) {
        return {42};
    } else if (v == rand()) {
        return {142};
    }
}

int main()
{
    const auto rnd = rand();
    std::cout << "rnd: " << rnd << std::endl;

    std::cout << bad(rnd).value << std::endl;

    return 0;
}


图片

编译器通过调用rand()(第16行)诚实地再现了所有预期的副作用,从而消除了对不确定行为过早开始的怀疑。

Windows MSVC 2019 16.5.4,/ Od / RTC


/ RTCs选项启用堆栈帧运行时错误检查。此选项仅在调试程序集中可用。考虑一下main()部分的反汇编代码:

图片

在调用bad(int)(第4行)之前,已准备好参数-rnd变量的值复制到edx寄存器(第2行),并且位于该地址的某些局部变量的有效地址已加载到rcx寄存器中rsp + 28h(第3行)。

大概rsp + 28是一个临时变量的地址,用于存储调用bad(int)的结果。

第19和20行确认了这一假设-将相同变量的有效地址加载到rcx中,然后调用析构函数。

但是,在第4-18行的间隔中,尽管输出了要流式传输其数据字段的值,但仍未访问此变量。

从前面的MSVC清单中可以看到,流输出运算符的参数应该在rdx寄存器中。rdx寄存器获得解引用rax中的地址的结果(第9行)。

因此,调用代码的期望值是错误的(int):

  • 填写地址通过rcx寄存器传递的变量(此处我们看到RVO起作用);
  • 通过rax寄存器返回该变量的地址。

让我们继续列出不好的(int):

图片

  • 在eax中,输入了0xCCCCCCCC的值,这在访问冲突消息(第9行)中看到(请注意,它只有4个字节,而在AccessViolation消息中,地址由8个字节组成);
  • 调用rep stos命令,从地址rdi(第10行)开始执行0xC个周期,将eax的内容写入内存。它们是48个字节-与第6行中的堆栈分配的字节数完全相同;
  • 在正确的执行路径上,将rsp + 40h中的值输入rax(第23、36行);
  • rcx寄存器的值(main()通过其传递目标地址)在rsp + 8处(第4行)被压入堆栈;
  • rdi被压入堆栈,这使rsp减少8(第5行);
  • 通过减少rsp(第6行)在堆栈上分配30h字节。

因此,第4行中的rsp + 8和其余代码中的rsp + 40h是相同的值。
该代码相当令人困惑 它不使用rbp。

在访问冲突消息中有两个事故:

  • 地址的上部为零-可能有垃圾;
  • 该地址偶然被发现是不正确的。

显然,/ RTCs选项启用了某些非零值的堆栈覆盖,并且Access Violation消息只是随机的副作用。

让我们看看打开/ RTCs选项的代码与没有它的代码有何不同。

图片

main()部分的代码仅在堆栈上局部变量的地址上有所不同。

图片

(为清楚起见,我在它旁边放置了两个错误的(int)函数版本-带/ RTC和不带/ RTC),
没有/ RTC,rep stos指令消失了,并在函数开始时为其准备了参数。

例子2a


同样,尝试控制不确定的行为。这次仅用于一个编译器。

Windows MSVC 2019 16.5.4,/ Od / RTC


使用/ RTCs选项,编译器将在错误(int)函数代码的开头插入,该代码以固定值填充rax的下半部分,这可能会导致访问冲突。

要更改此行为,只需在rax中填充一些有效地址即可。
这可以通过一个非常简单的修改来实现:将std :: cout的内容输出添加到错误的(int)主体中。

完整的示例代码
#include <iostream>
#include <stdlib.h>

struct Test
{
    Test(uint64_t v)
        : value(v)
    {
        std::cout << "Test::Test(" << v << ")" << std::endl;
    }
    ~Test()
    {
        std::cout << "Test::~Test()" << std::endl;
    }

    uint64_t value;
};

Test bad(int v)
{
  std::cout << "rnd: " << v << std::endl;
  
  if (v == 0) {
        return {42};
    } else if (v == 1) {
        return {142};
    }
}

int main()
{
    const auto rnd = rand();

    std::cout << bad(rnd).value << std::endl;

    return 0;
}


rnd:41
8791039331928
测试::〜测试()

运算符<<返回到流的链接,该链接的实现是将地址std :: cout放置在rax中。地址正确,可以取消引用。防止访问冲突。

结论


使用最简单的示例,我们能够:

  • 收集大约十种不确定行为的表现;
  • 详细了解这些选项将如何执行。

所有编译器都严格遵守该标准-在任何情况下,不确定行为都没有更早开始。但是您不能拒绝编译器开发人员的幻想。

通常,表现形式取决于细微差别:值得添加或删除似乎无关的代码行-程序的行为会发生很大变化。

显然,不编写此类代码要比以后解决难题更容易。

All Articles