为了用上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);

判断条件很多,STB_LOCALsi >= 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_info0


而查看导出的符号的话,.dynsym里有432个条目:

其中第一个是STB_LOCAL


参考前面两份代码,ELF的规定应该是LOCAL的符号必须在sh_info指定的下标之前。

那么直接修改sh_info1,让它指向第一个GLOBAL的符号就可以了。


PS:其实我之前还尝试了修改sh_offset+sh_size,以及修改st_info,但它们都不奏效……


总结:

不要没事找事,遇到问题最好绕着走……

参考

  1. ^OpenCLUtil https://github.com/XZiar/RayRenderer/tree/master/OpenCLUtil
  2. ^OpenCL-ICD-Loader https://github.com/KhronosGroup/OpenCL-ICD-Loader
  3. ^clintercept https://github.com/intel/opencl-intercept-layer
  4. ^libglvnd https://github.com/NVIDIA/libglvnd
编辑于 2021-06-29 23:38