为了用上OpenCL,被逼0基础修复ELF……
最近比较懒,不想写正经代码,又把心思投入到了将项目适配到装了termux的安卓老爷机上。
结果,比写正经代码还累……
目标是让OpenCLUtil[1]跑起来,而这个库又依赖于OpenCL-ICD-Loader[2]。
编译起来其实没什么问题,link也成功,但最终跑起来却又得到了恼人的错误:
CANNOT LINK EXECUTABLE: library "libOpenCL.so" not found
这里的libOpenCL.so其实是ICD Loader。之所以这么命名是因为,之前使用过程中被告知,只有当程序加载名为OpenCL的库时,intel的clintercept[3]才能正确hook……
总之研究了好一会儿,发现只要让OpenCLUtil不去链接OpenCL,转而让最终的executable去链接,就没问题了……可能是链接顺序不对吧。
总之现在程序能跑起来了,但ICD Loader却找不到任何platform。
其实说得通,ICD Loader需要检索特定信息来确认每个vendor的库在哪。
对于Windows,这个信息通常保存在注册表里,每个adaptor会有一整串信息记录着各种UserModeDriver的dll路径,比如在这里:
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0002
可以看到这里不但有OpenCL Driver路径,还有OpenGL、Vulkan、LevelZero(OneAPI)的ICD Driver路径。
PS,OpenGL没有提供多适配器选择功能,原本有类似ICD Loader的提案的,但最终发展方向转向vulkan了。NV倒是出了个libglvnd[4],但只能用于Linux。如果想要实现在app内手动选择多适配器,估计要自己实现一套ICD Loader。
扯远了,Windows常见的做法是信息放注册表,*Nix下当然就是往特定目录放单独的文件了。
/etc/OpenCL/vendors
这个目录下各家通常会放一个xx.icd文件,其内容就是so库的路径:
而Android又不太一样,ICD Loader里官方给的路径是:
/system/vendor/Khronos/OpenCL/vendors
但在我这台机子上是没有见到这个目录……
不过这不代表这不支持OpenCL,因为实际的so库在/system/vendor/lib64/
下,上可以手动创建icd文件去注册支持。
考虑到系统目录不一定有读写权限,ICD Loader还支持通过环境变量来手动指定icd文件的检索目录:
envPath = khrIcd_secure_getenv("OCL_ICD_VENDORS");
if (NULL != envPath)
{
vendorPath = envPath;
}
所以找个目录创建好icd文件,再配置一下环境变量OCL_ICD_VENDORS
就行了。
然而结果还是不行,依旧返回0个platform。
没办法,只能上lldb去单步跟踪一下,看看有没有正确检索到so库。lldb的用法也是现学的,没有gui的debug实在是让人头大……
结果发现so库其实被成功定位到了,甚至成功加载了。问题出在clIcdGetPlatformIDs
上:
// get the library's clGetExtensionFunctionAddress pointer
p_clGetExtensionFunctionAddress = (pfn_clGetExtensionFunctionAddress)(size_t)khrIcdOsLibraryGetFunctionAddress(library, "clGetExtensionFunctionAddress");
if (!p_clGetExtensionFunctionAddress)
{
KHR_ICD_TRACE("failed to get function address clGetExtensionFunctionAddress\n");
goto Done;
}
// use that function to get the clIcdGetPlatformIDsKHR function pointer
p_clIcdGetPlatformIDs = (pfn_clIcdGetPlatformIDs)(size_t)p_clGetExtensionFunctionAddress("clIcdGetPlatformIDsKHR");
if (!p_clIcdGetPlatformIDs)
{
KHR_ICD_TRACE("failed to get extension function address clIcdGetPlatformIDsKHR\n");
goto Done;
}
加载库的操作是首先获取clGetExtensionFunctionAddress
,然后通过它去获取clIcdGetPlatformIDs
。但在我这个老爷机(高通625)上,没有返回这个函数的指针……
事实上直接去查库的导出表的话,这个函数是在的……
~ $ nm -gDC /system/vendor/lib64/libOpenCL.so | grep clIcdGetPlatformIDs
00000000000102ac T clIcdGetPlatformIDsKHR
000000000000d4d4 T qCLDefaultAPI_clIcdGetPlatformIDsKHR
000000000000d23c T qCLDrvAPI_clIcdGetPlatformIDsKHR
我专门去翻了下spec,情况愈发复杂了……
OpenCL1.2里把这个函数给deprecated了……
但是OpenCL2.0的cl_khr_icd
扩展又明确列出了加载方式,和前面ICD Loader的做法完全一致(毕竟是自家的库)……
Upon successfully loading a Vendor ICD's library, the ICD Loader queries the following functions from the library: clIcdGetPlatformIDsKHR, clGetPlatformInfo, andclGetExtensionFunctionAddress
. If any of these functions are not present then the ICD Loader will close and ignore the library.
Next the ICD Loader queries available ICD-enabled platforms in the library using clIcdGetPlatformIDsKHR. For each of these platforms, the ICD Loader queries the platform's extension string to verify thatcl_khr_icd
is supported, then queries the platform's Vendor ICD extension suffix using clGetPlatformInfo with the valueCL_PLATFORM_ICD_SUFFIX_KHR
.
If any of these steps fail, the ICD Loader will ignore the Vendor ICD and continue on to the next.
看来问题还是出在这个库本身。
ICD Loader不行,那就绕开直接链接vendor给的库呗。反正ICD Loader的最大用途其实是方便在多个platform间自由选择,而手机上只有一个vendor。
于是又改工具链去排除掉了ICD Loader,直接创建了个软链接——成功。
platform[0] QUALCOMM Snapdragon(TM) {OpenCL 2.0 QUALCOMM build: commit #2371bd1 changeid #I8ebe47d372 Date: 03/12/18 Mon Local Branch: Remote Branch: quic/gfx-adreno.lnx.1.0.r36-rel}
device[0] [0000:00.0 ]QUALCOMM Adreno(TM) {OpenCL 2.0 Adreno(TM) 506 | OpenCL C 2.0 Adreno(TM) 506} [1 CU]
[OCLStub]Create context with [QUALCOMM Adreno(TM)] on [QUALCOMM Snapdragon(TM)]!
然后一查extension才发现根本就查不到cl_khr_icd,即不支持这个扩展!怪不得ICD Loader不能用……
说了这么多,好像都没讲到题目里的修复ELF?
别急,这就来了……
前面只是通过软链接使得程序自动加载了这个库。但如果重新编译的话,由于加了-lOpenCL
,linker会尝试去链接这个库的,然后问题又来了:
ld.lld: error: /data/data/com.termux/files/Projects/RayRenderer/ARM64/Debug/libOpenCL.so: invalid sh_info in symbol table
clang-12: error: linker command failed with exit code 1 (use -v to see invocation)
链接失败,linker报错说invalid sh_info
……
而如果拿readelf去测试一下的话也能直接发现一个warning:
readelf: Warning: local symbol 0 found at index >= .dynsym's sh_info value of 0
好家伙,结果这个libOpenCL.so
本身居然还是坏的?
不过之前直接加载没有问题,readelf也只是报了个warning,看来不是什么大问题,只是lld的要求比较严格而已吧。
原本应该考虑直接换用其他linker,但termux下只有llvm,而且似乎只能用lld……
不死心,网上搜了好一会儿也没解决方案。
不过反正readelf和lld都是开源的,不如顺便去看看他们到底做了什么检查?
readelf的内容在这:
源文件太长,github上浏览容易卡死。不过下载下来搜索可以看到关键部分:
if (ELF_ST_BIND (psym->st_info) == STB_LOCAL
&& section != NULL
&& si >= section->sh_info
/* Irix 5 and 6 MIPS binaries are known to ignore this requirement. */
&& filedata->file_header.e_machine != EM_MIPS
/* Solaris binaries have been found to violate this requirement as
well. Not sure if this is a bug or an ABI requirement. */
&& filedata->file_header.e_ident[EI_OSABI] != ELFOSABI_SOLARIS)
warn (_("local symbol %lu found at index >= %s's sh_info value of %u\n"),
si, printable_section_name (filedata, section), section->sh_info);
判断条件很多,STB_LOCAL
、si >= section->sh_info
是比较关键的两个条件。
而lld的代码在这:
关键部分是:
firstGlobal = symtabSec->sh_info;
ArrayRef<Elf_Sym> eSyms = CHECK(obj.symbols(symtabSec), this);
if (firstGlobal == 0 || firstGlobal > eSyms.size())
fatal(toString(this) + ": invalid sh_info in symbol table");
这边的判断又不太一样,直接把sh_info
当做了“第一个GLOBAL”,并且要求不为0……
那实际的so文件到底是怎么一回事呢?
找了个在线查看/修改ELF的网站:
Section headers可以看到:
sh_info
是0
。
而查看导出的符号的话,.dynsym里有432个条目:
其中第一个是STB_LOCAL
。
参考前面两份代码,ELF的规定应该是LOCAL
的符号必须在sh_info
指定的下标之前。
那么直接修改sh_info
为1
,让它指向第一个GLOBAL
的符号就可以了。
PS:其实我之前还尝试了修改sh_offset
+sh_size
,以及修改st_info
,但它们都不奏效……
总结:
不要没事找事,遇到问题最好绕着走……
参考
- ^OpenCLUtil https://github.com/XZiar/RayRenderer/tree/master/OpenCLUtil
- ^OpenCL-ICD-Loader https://github.com/KhronosGroup/OpenCL-ICD-Loader
- ^clintercept https://github.com/intel/opencl-intercept-layer
- ^libglvnd https://github.com/NVIDIA/libglvnd