为了用上OpenCL,被逼0基础修复ELF……

为了用上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 , and clGetExtensionFunctionAddress . 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 that cl_khr_icd is supported, then queries the platform's Vendor ICD extension suffix using clGetPlatformInfo with the value CL_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);