在我的编程习惯意识里,布尔 bool 只能是 true 或 false 的值。如果要算上可空布尔,那就最多加上一个 null 空值。然而从 dotnet 运行时的角度上说,布尔完全完全可以是 true 或 false 之外的值

试试看以下的代码片段,猜猜将会输出什么内容

using System.Runtime.CompilerServices;

byte t = 2;
var foo = Unsafe.As<byte, bool>(ref t);
Console.WriteLine(foo);

if (foo)
{
    Console.WriteLine($"if (foo.F1)");
}

if (foo == true)
{
    Console.WriteLine($"if (foo.F1 == true)");
}
else
{
    Console.WriteLine($"if (foo.F1 != true)");
}

var t1 = true;
if (foo == t1)
{
    Console.WriteLine($"if (foo.F1 == t1)");
}
else
{
    Console.WriteLine($"if (foo.F1 != t1)");
}

有些伙伴也许和我一样,在看到前面几句话时,就开始感觉到不妙。正如 var foo = Unsafe.As<byte, bool>(ref t); 代码所示,将一个 byte t = 2; 塞到 bool foo 里面。这似乎看起来很有违和感,但在 dotnet 运行时里面却又是合法的行为。因为 bool 的长度也和 byte 相同

这就有趣起来了,众所周知,在 dotnet C# 里面的布尔 true 和 false 分别是 1 和 0 的值。如果我强行将一个 2 塞到布尔里面,那会发生什么事情呢?此时的 foo 还是一个合法的布尔值么

答案是 foo 仍然是一个布尔值,如 if (foo) 判断绝对能够通过。即 Console.WriteLine(foo); 能够输出 true 值,且 if (foo) 能判断通过,输出 Console.WriteLine($"if (foo.F1)"); 信息

if (foo == true) 呢?要知道,现在 foo 是 2 的值,而不是为 1true 值。大家猜猜这个判断能否通过呢

开答案,那是能够通过的啦。这是为什么呢?其实这个判断条件通过更大的关系是和编译器相关。聪明的编译器认为 if (foo == true) 中的 == true 可以省略掉,于是 IL 代码里面其实是这样的

    IL_008b: ldloc.3      // foo
    IL_008c: stloc.s      V_7

    IL_008e: ldloc.s      V_7
    IL_0090: brfalse.s    IL_00a1

    // [21 1 - 21 2]
    IL_0092: nop

    // [22 5 - 22 47]
    IL_0093: ldstr        "if (foo.F1 == true)"
    IL_0098: call         void [System.Console]System.Console::WriteLine(string)
    IL_009d: nop

    // [23 1 - 23 2]
    IL_009e: nop

    IL_009f: br.s         IL_00ae

    // [25 1 - 25 2]
    IL_00a1: nop

    // [26 5 - 26 47]
    IL_00a2: ldstr        "if (foo.F1 != true)"
    IL_00a7: call         void [System.Console]System.Console::WriteLine(string)
    IL_00ac: nop

重新将 IL 翻译为低级 C# 代码如下

    if (foo)
    {
        Console.WriteLine("if (foo.F1 == true)");
    }
    else
    {
        Console.WriteLine("if (foo.F1 != true)");
    }

可以看到完全将 == true 忽略掉了

按照计算机习惯,在 if 判断里面,非 0 就是 true 值。于是为 2foo 也就能通过 if 的判断啦

但如本文一开始的代码所示,直接和一个布尔变量判断呢?如以下代码所示,此时还能输出什么呢

var t1 = true;
if (foo == t1)
{
    Console.WriteLine($"if (foo.F1 == t1)");
}
else
{
    Console.WriteLine($"if (foo.F1 != t1)");
}

本次的答案就是输出了 if (foo.F1 != t1) 内容,证明和 t1 判断不相等。这是因为此时聪明的编译器不敢进行省略 t1 的值,毕竟这个变量没有标记常量,也许开发者有奇怪的意图呢。于是就真真的执行了一次判断逻辑,其 IL 代码如下

    // [29 1 - 29 15]
    IL_00ae: ldc.i4.1
    IL_00af: stloc.s      t1

    // [30 1 - 30 15]
    IL_00b1: ldloc.3      // foo
    IL_00b2: ldloc.s      t1
    IL_00b4: ceq
    IL_00b6: stloc.s      V_8

    IL_00b8: ldloc.s      V_8
    IL_00ba: brfalse.s    IL_00cb

    // [31 1 - 31 2]
    IL_00bc: nop

    // [32 5 - 32 45]
    IL_00bd: ldstr        "if (foo.F1 == t1)"
    IL_00c2: call         void [System.Console]System.Console::WriteLine(string)
    IL_00c7: nop

    // [33 1 - 33 2]
    IL_00c8: nop

    IL_00c9: br.s         IL_00d8

    // [35 1 - 35 2]
    IL_00cb: nop

    // [36 5 - 36 45]
    IL_00cc: ldstr        "if (foo.F1 != t1)"
    IL_00d1: call         void [System.Console]System.Console::WriteLine(string)
    IL_00d6: nop

以上 IL 翻译回 C# 代码和原本的代码一样,就真真的用了 ceq 去判断了 foo 和 t1 是否相等。由于 foo 是 2 的值,而 t1 是 1 的值,自然从内存的角度上就返回了不相等

本文一开始的代码的最终输出内容如下

True
if (foo.F1)
if (foo.F1 == true)
if (foo.F1 != t1)

这是一个非常有趣的事情,可以用来编写一些让静态代码审核完全阵亡的代码。比如说从一个数据接收源里面,将一个布尔放入到某个结构体里面,再利用 MemoryMarshal 或 Unsafe.As 等方式将数据直接拍到结构体上面。于是此时就可以在结构体里面制造出一个不为 true 或 false 的布尔字段

再在代码逻辑里面,尝试使用此与一个可为空的布尔变量进行判断。对于可为空的布尔判断,额外写一个 else 也不过分吧,毕竟人家还有 null 值呢。于是代码审查将会看到一段必不为空的分支里面,走进了 else 分支里面去了。估计没有对应的数据配合调试,依靠静态代码审查是完全看不出来的

恭喜你,又学了一样可能没有用的技能,和一项绕过静态代码审查的坏知识

好开发者们可不用将此技术用在自己的实际产品项目中哦,不然开发时要带好头盔,免得被其他开发伙伴用板砖拍了

本文代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin e3c800f41d3f0ed46997cb69baa1bd5e87c576a6

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin e3c800f41d3f0ed46997cb69baa1bd5e87c576a6

获取代码之后,进入 Workbench/DeharkallairheaLawgijewho 文件夹,即可获取到源代码

更多技术博客,请参阅 博客导航


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-C-%E5%B8%83%E5%B0%94%E5%8F%AF%E4%BB%A5%E6%98%AF-true-%E6%88%96-false-%E5%A4%96%E7%9A%84%E5%80%BC.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利