函数直接返回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,al
,al
表示RAX
寄存的低8位, 而函数的返回值就是保存在RAX
寄存中,所以对于返回值是bool
的C++
函数,直接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], al
将GetBool1()
的保存在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