当前位置: 首页 > news >正文

【C++】函数直接返回bool值和返回bool变量差异

函数直接返回bool值和返回bool变量差异

背景

在工作中遇到一个比较诡异的问题,场景是给业务方提供的SDK有一个获取状态的函数GetStatus,函数的返回值类型是bool,在测试过程中发现,SDK返回的是false,但是业务方拿到的返回值是true。SDK是C语言和C++语言编写的,C语言编写接口层, C++语言编写实际逻辑,业务方是unity,使用C#语言,通过DllImport引用SDK DLL

  • DllImport声明如下
[DllImport("SDK.dll", EntryPoint="GetStatus", CharSet=CharSet.Ansi, 
CallingConventin=CallingConvention.Cdec1)]
public static extern bool GetStatus([MarshalAs(UnmanagedType.LPStr)] string key);
  • C语言接口层
// 声明
__declspec(dllexport) bool GetStatus(const char* key); 
// 定义
bool GetStatus(const char *key)
{
	return cpp_instance->GetStatus(key);
}
  • C++实现
    C++ 将key对应的状态保存到一个map中,key类型为std::string,值为std::any
template<class typename T>
T GetStatus(const std::string &key)
{
	if(!status_map_.count(key)) {
		return {};
	}
	const auto &value = status_map_.at(key);
	if(!value.has_value()) {
		return {};
	}
	try {
		return std::any_cast<T>(value);
	} catch (const std::exception &e) {
		return {};
	}
}

排查

C++侧排查

通过 SDK的 unity demo 调试未能复现问题,将SDK Attach到业务进程调试问题复现,由于代码中没有中间变量保留结果,都是直接将结果返回。调试不方便。在代码中增加了一行日志打印。结果问题不复现了。

  • 增加一行日志后的代码
bool GetStatus(const char *key)
{
   auto ret = cpp_instance->GetStatus(key);
   std::cout << "get status result " << ret << std::endl;
   return ret;
}

百思不得其解,决定反汇编调试看下,看下增加日志前和增加日志后的汇编代码。
为了方便调试和说明,这里编写了复现的简单demo,如下:

#include <iostream>

typedef int (*GetBoolFuncPtr)();


static bool GetBool1()
{
    return {};
}
static bool GetBool2()
{
  /*  bool ret = GetBool1();
    std::cout << "ret:" << ret << std::endl;
    return ret;*/
    return GetBool1();
}

int main()
{

    /*GetBoolFuncPtr booll = reinterpret_cast<int (*)()>(GetBool1);
    GetBoolFuncPtr bool2 = reinterpret_cast<int (*)()>(GetBool2);
    std::cout << booll() << " " << bool2() << std::endl;*/
	std::cout << GetBool2() << std::endl;
    int input;
    std::cin >> input;
    return 0;
}
未增加日志打印的反汇编代码如下

未增加日志的反汇编代码
可以看到GetBool2()函数中直接返回了GetBool1()函数的结果,return {}的反汇编代码为xor al,alal表示RAX寄存的低8位, 而函数的返回值就是保存在RAX寄存中,所以对于返回值是boolC++函数,直接return {}是将RAX寄存器的低8位清零,RAX的其他位数是残值。之所以只清零低8位是因为在C++中bool 占1个字节。

增加日志的输出代码
static bool GetBool2()
{
    bool ret = GetBool1();
    std::cout << "ret:" << ret << std::endl;
    return ret;
}

反汇编代码
增加日志的反汇编代码
可以看到GetBool2()函数中通过一个中间变量ret保存了GetBool1()的返回值,并且打印ret的值,然后将ret返回,通过反汇编代码可以看到move byte ptr [ret], alGetBool1()的保存在al的值保存在了ret指向的地址,return ret对应的反汇编代码movezx eax, byte ptr [ret]ret的值保存到 eax寄存器,eax寄存器是RAX寄存器的低32位。这里的重点是movezx指令,movezx指令可以将较小的值用0扩展到较大的值。所以这里eax的高24位被清零。

C++侧排查总结
  • return {}返回bool值将清零RAX寄存器的低8位
  • return ret 返回bool值将清零RAX寄存器的低32位
    可以看到 增加了一行日志和没有这行日志的差别在于会清零返回值寄存器RAX的多少位。这个差别为什么会导致unity 拿到的结果不一样呢?需要继续排查。
unity排查
现状
  • 业务unity应用出现问题

  • SDK unity demo正常
    通过和业务开发沟通发现,业务unity应用后端使用的是il2cpp模式,而SDK unity demo则使用的是mono模式,也许是这里出现了问题,果然将unity demo的后端改成il2cpp模式后复现问题。

    那为什么在il2cpp模式下有问题呢?

    il2cpp模式会将C#代码转换成cpp代码,业务同学说在C#的DllImport地方增加解决了

    [return: MarshalAs(UnmanagedType.I1)]
    

    为什么增加这行代码就可以了,原来在C#中的UnmanagedType类型中bool变量是占4个字节,而使用il2cpp模式后C#的代码会被转换成C++代码。Dllimport的代码会转换成类似下面的逻辑(这里只摘出了重要代码)

    typedef  int32_t (CDECL * PInvokeFunc)(char*);
    static PInvokeFunc  il2cppPInvokeFunc;
    if(il2cppPInvokeFunc == NULL) {
    	il2cppPInvokeFunc  = il2cpp_codegen_resolve_pinvoke<PInvokeFunc>(IL2CPP_NATIVE_STRING("SDK.dll"), "GetStatus", ....);
    }
    
结论

从转换后的代码可以看到,il2cpp C++代码解析的GetStatus函数的返回值是int32_t,也就是说SDK内部返回bool的函数,这里被解析成int32_t了,返回int32_t会从返回值寄存器RAX中读取低32位,结合前面的C++demo分析可以解释了为什么没有打印日志拿到的返回值大概率是true,因为没有打印日志,返回 false只清零了RAX寄存器的低8位,而il2cpp中读取了32位,RAX寄存器大概率有其他函数调用的残值,导致il2cpp中读取到的值大概率为true。而打印了日志,返回false清零RAX寄存器的低32位, il2cpp代码读取正确

参考

https://stackoverflow.com/questions/20035826/why-dllimport-for-c-bool-as-unmanagedtype-i1-throws-but-as-byte-it-works
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedtype?view=net-9.0

相关文章:

  • 使用 redis 实现消息队列
  • 【leetcode hot 100 70】爬楼梯
  • 万相2.1本地部署教程——阿里开源超强AI视频模型:物理模拟×中英双驱,定义AI生成新标杆!
  • almalinux 8 9 升级到指定版本
  • Ansible的使用2
  • 一周学会Pandas2 Python数据处理与分析-Pandas2二维数据结构-DataFrame
  • ruby超高级语法
  • Jenkins配置的JDK,Maven和Git
  • 一站式AIGC创作平台主要功能介绍及使用教程
  • C语言核心知识点整理:结构体对齐、预处理、文件操作与Makefile
  • GPT文生图模型新玩法
  • DFS和BFS的模版
  • Linux 基础入门操作 前言 VIM的基本操作 2
  • Java logback框架日志输出中文乱码的解决方案(windows)
  • 【Easylive】定时任务-每日数据统计和临时文件清理
  • JavaWeb 课堂笔记 —— 04 Ajax
  • 我提了一个 Androidx IssueTracker
  • [QMT量化交易小白入门]-四十二、五年年化收益率26%,当日未成交的下单,取消后重新委托
  • PHP开发效率提升利器:通义灵码在VSCode中的应用与技巧
  • Model Context Protocol(MCP)介绍
  • 瑞士联邦主席凯勒-祖特尔、联邦副主席帕姆兰会见何立峰
  • 雷军:过去一个多月是创办小米以来最艰难的时间
  • 呼和浩特推进新一轮国企重组整合:杜绝一项目一公司、一业务一公司
  • 图集|俄罗斯举行纪念苏联伟大卫国战争胜利80周年阅兵式
  • “毛茸茸”的画,诗意、温暖又治愈
  • 报告:4月份新增发行的1763亿元专项债中,投向房地产相关领域约717亿元