วิธีการเชื่อมโยงรันไทม์ตามความสามารถของ CPU บน linux

เป็นไปได้ไหมที่ไลบรารี linux (เช่น "libloader.so") โหลดไลบรารีอื่นเพื่อแก้ไขสัญลักษณ์ภายนอก

ฉันมีโค้ดจำนวนมากที่ได้รับการคอมไพล์ตามเงื่อนไขเพื่อให้รองรับระดับ SIMD ( SSE2, AVX, AVX2 ) วิธีนี้ใช้ได้ผลดีหากแพลตฟอร์ม build เหมือนกับแพลตฟอร์มรันไทม์ แต่มันขัดขวางการนำโปรเซสเซอร์รุ่นต่างๆ มาใช้ซ้ำ

ความคิดหนึ่งคือการมี executable ซึ่งเรียก function เชื่อมโยงไปยัง libloader.so ซึ่งไม่ได้ใช้ function โดยตรง แต่จะแก้ไข (ผูก?) สัญลักษณ์นั้นจากไลบรารีที่โหลดอื่นเช่น libimpl_sse2.so, libimpl_avx2.so หรือมากกว่านั้นขึ้นอยู่กับ cpuflags

มีฟังก์ชันนับร้อยที่ต้องผูกแบบไดนามิกในลักษณะนี้ ดังนั้นการเปลี่ยนการประกาศหรือรหัสการโทรจึงไม่สามารถทำได้ การเชื่อมโยงโปรแกรมค่อนข้างง่ายที่จะเปลี่ยนแปลง ตัวแปรสภาพแวดล้อมรันไทม์สามารถเปลี่ยนแปลงได้ แต่ฉันไม่ต้องการ

ฉันได้ไกลถึงการสร้างปฏิบัติการที่สร้างและเริ่มต้นด้วยสัญลักษณ์ภายนอกที่ไม่ได้รับการแก้ไข (UES) ผ่านทางแฟล็ก ld --unresolved-symbols=ignore-all แต่การโหลด impl lib ในภายหลังจะไม่เปลี่ยนค่าของฟังก์ชัน UES จาก NULL


person Mark Borgerding    schedule 08.04.2015    source แหล่งที่มา
comment
สิ่งนี้อาจไม่สามารถใช้ได้กับกรณีของคุณ แต่ด้วย Intel Compiler คุณสามารถทำได้โดยใช้ตระกูลแฟล็ก -ax* (เช่น หากคุณต้องการให้พาธโค้ด SSE และ AVX ใช้งานได้ทั้งคู่ - ให้ใช้ -axAVX ระหว่างการคอมไพล์)   -  person zam    schedule 12.04.2015
comment
@zam ขอบคุณ แต่ -ax ดูเหมือนจะใช้กับการเพิ่มประสิทธิภาพอัตโนมัติ (สร้างคอมไพเลอร์) บน Intel เท่านั้น ฉันต้องการโซลูชันที่อนุญาตให้รวบรวมเส้นทาง #ifdef แยกกัน มันจะต้องทำงานร่วมกับ gcc ด้วย   -  person Mark Borgerding    schedule 12.04.2015


คำตอบ (1)


แก้ไข: ฉันพบในภายหลังว่าเทคนิคที่อธิบายด้านล่างนี้จะใช้ได้ภายใต้สถานการณ์ที่จำกัดเท่านั้น โดยเฉพาะอย่างยิ่ง ไลบรารีที่แบ่งใช้ของคุณต้องมีเฉพาะฟังก์ชันเท่านั้น โดยไม่มีตัวแปรโกลบอลใดๆ หากมีโกลบอลภายในไลบรารีที่คุณต้องการส่งไป คุณจะพบกับข้อผิดพลาดรันไทม์ไดนามิกลิงเกอร์ สิ่งนี้เกิดขึ้น เนื่องจากตัวแปรส่วนกลางถูกย้ายก่อนที่จะเรียกใช้ตัวสร้างไลบรารีที่ใช้ร่วมกัน< /ก>. ดังนั้น ตัวเชื่อมโยงจำเป็นต้องแก้ไขการอ้างอิงเหล่านั้นตั้งแต่เนิ่นๆ ก่อนที่แผนการจัดส่งที่อธิบายไว้ที่นี่จะมีโอกาสทำงาน


วิธีหนึ่งในการบรรลุสิ่งที่คุณต้องการคือ (ab) ใช้ฟิลด์ DT_SONAME ในส่วนหัว ELF ของไลบรารีที่แบ่งใช้ของคุณ ซึ่งสามารถใช้เพื่อเปลี่ยนชื่อไฟล์ที่ตัวโหลดแบบไดนามิก (ld-linux-so*) โหลดขณะรันไทม์เพื่อแก้ไขการพึ่งพาไลบรารีแบบแบ่งใช้ นี่เป็นการอธิบายที่ดีที่สุดด้วยตัวอย่าง สมมติว่าฉันรวบรวมไลบรารีที่แบ่งใช้ libtest.so ด้วยบรรทัดคำสั่งต่อไปนี้:

g++ test.cc -shared -o libtest.so -Wl,-soname,libtest_dispatch.so

สิ่งนี้จะสร้างไลบรารีที่ใช้ร่วมกันซึ่งมีชื่อไฟล์เป็น libtest.so แต่ฟิลด์ DT_SONAME ถูกตั้งค่าเป็น libtest_dispatch.so มาดูกันว่าเกิดอะไรขึ้นเมื่อเราเชื่อมโยงโปรแกรมเข้ากับมัน:

g++ testprog.cc -o test -ltest

มาตรวจสอบการขึ้นต่อกันของไลบรารีรันไทม์สำหรับไบนารีของแอปพลิเคชันผลลัพธ์ test:

> ldd test
linux-vdso.so.1 =>  (0x00007fffcc5fe000)
libtest_dispatch.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd1e4a55000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd1e4e4f000)

โปรดทราบว่าแทนที่จะค้นหา libtest.so ตัวโหลดแบบไดนามิกต้องการโหลด libtest_dispatch.so แทน คุณสามารถใช้ประโยชน์จากสิ่งนี้เพื่อใช้ฟังก์ชันการจัดส่งที่คุณต้องการ ฉันจะทำอย่างไร:

  • สร้างไลบรารีที่ใช้ร่วมกันของคุณเวอร์ชันต่างๆ ฉันคิดว่ามีเวอร์ชัน "ทั่วไป" บางเวอร์ชันที่สามารถใช้งานได้ตลอดเวลา โดยมีเวอร์ชันที่ได้รับการปรับปรุงอื่น ๆ ที่ใช้งานรันไทม์ตามความเหมาะสม ฉันจะตั้งชื่อเวอร์ชันทั่วไปด้วยชื่อไลบรารี "ธรรมดา" libtest.so และตั้งชื่อเวอร์ชันอื่นๆ ตามที่คุณต้องการ (เช่น libtest_sse2.so, libtest_avx.so ฯลฯ)

  • เมื่อลิงก์เวอร์ชันทั่วไปของไลบรารี ให้แทนที่ DT_SONAME กับเวอร์ชันอื่น เช่น libtest_dispatch.so

  • สร้างไลบรารีโปรแกรมเลือกจ่ายงานชื่อ libtest_dispatch.so เมื่อโหลดโปรแกรมเลือกจ่ายงานเมื่อเริ่มต้นแอปพลิเคชัน จะมีหน้าที่โหลดการใช้งานไลบรารีที่เหมาะสม ต่อไปนี้เป็นรหัสเทียมสำหรับลักษณะการใช้งาน libtest_dispatch.so:

    #include <dlfcn.h>
    #include <stdlib.h>
    
    // the __attribute__ ensures that this function is called when the library is loaded
    __attribute__((constructor)) void init()
    {
        // manually load the appropriate shared library based upon what the CPU supports
        // at runtime
        if (avx_is_available) dlopen("libtest_avx.so", RTLD_NOW | RTLD_GLOBAL);
        else if (sse2_is_available) dlopen("libtest_sse2.so", RTLD_NOW | RTLD_GLOBAL);
        else dlopen("libtest.so", RTLD_NOW | RTLD_GLOBAL);
        // NOTE: this is just an example; you should check the return values from 
        // dlopen() above and handle errors accordingly
    }
    
  • เมื่อลิงก์แอปพลิเคชันกับไลบรารีของคุณ ให้ลิงก์แอปพลิเคชันนั้นกับ "vanilla" libtest.so ซึ่งเป็นอันที่มีการแทนที่ DT_SONAME เพื่อชี้ไปที่ไลบรารีของโปรแกรมเลือกจ่ายงาน สิ่งนี้ทำให้การจัดส่งมีความโปร่งใสสำหรับผู้เขียนแอปพลิเคชันใดๆ ที่ใช้ไลบรารีของคุณ

สิ่งนี้ควรใช้งานได้ตามที่อธิบายไว้ข้างต้นบน Linux บน Mac OS ไลบรารีที่ใช้ร่วมกันจะมี "ชื่อการติดตั้ง" ที่คล้ายคลึงกับ DT_SONAME ที่ใช้ในไลบรารีที่ใช้ร่วมกันของ ELF ดังนั้นจึงสามารถใช้กระบวนการที่คล้ายกับขั้นตอนข้างต้นแทนได้ ฉันไม่แน่ใจว่าสิ่งที่คล้ายกันนี้สามารถใช้กับ Windows ได้หรือไม่

หมายเหตุ: มีสมมติฐานสำคัญประการหนึ่งที่ทำขึ้นข้างต้น: ความเข้ากันได้ของ ABI ระหว่างการใช้งานต่างๆ ของไลบรารี นั่นคือ ไลบรารีของคุณควรได้รับการออกแบบให้มีความปลอดภัยในการเชื่อมโยงกับเวอร์ชันทั่วไปส่วนใหญ่ในเวลาลิงก์ ในขณะที่ใช้เวอร์ชันที่ได้รับการปรับปรุง (เช่น libtest_avx.so) ขณะรันไทม์

person Jason R    schedule 10.04.2015
comment
คำตอบที่ดี พรสวรรค์ในการแก้ปัญหาของคุณนั้นขึ้นอยู่กับความสามารถในการอธิบายของคุณเท่านั้น - person Mark Borgerding; 10.04.2015
comment
หมายเหตุประการหนึ่ง: บน Mac OS เมื่อคุณเชื่อมโยงแอปพลิเคชันของคุณกับ libtest.so คุณจะต้องระบุแฟล็กของลิงก์เกอร์ -flat_namespace ลักษณะการทำงานเริ่มต้นคือการกำหนดให้สัญลักษณ์นั้นมาจากไลบรารีที่คาดว่าจะมาจาก ในกรณีนี้ นั่นหมายความว่า libtest_dispatch.so จะต้องรับผิดชอบในการจัดเตรียมสัญลักษณ์ทั้งหมด หากคุณเปิดใช้งานเนมสเปซแบบแบน มันจะผูกสัญลักษณ์เข้ากับไลบรารีอื่นที่โปรแกรมเลือกจ่ายงานโหลด ซึ่งดังที่หน้าคู่มือ ld ระบุ เหมือนกับพฤติกรรมบนระบบปฏิบัติการอื่น (เช่น Linux) มากกว่า - person Jason R; 10.04.2015