במכשירים עם Android מגרסה 10 (API ברמה 29) ואילך, אפשר להשתמש בשכבות של OpenGL ES (GLES). אפליקציה שניתנת לניפוי באגים יכולה לטעון שכבות GLES מקובץ ה-APK שלה, מהספרייה הבסיסית שלה או מקובץ APK של שכבה שנבחרה.
השימוש בשכבת GLES דומה לשימוש בשכבת האימות של Vulkan.
הדרישות
יש תמיכה בשכבות GLES רק בגרסאות GLES 2.0 ואילך.
אתחול שכבה
אחרי שאוכלסים נקודות הכניסה הסטנדרטיות, EGL Loader יוצר מופע של GLESLayerLoader
. אם שכבות ניפוי הבאגים מופעלות, הפונקציה LayerLoader
סורקת ספריות ספציפיות כדי לאתר שכבות, כמו מטען Vulkan.
אם האפשרות ליצירת שכבות מופעלת, LayerLoader
מחפש רשימת שכבות ספציפית ומספור אותה. רשימת השכבות מצוינה באמצעות שמות קבצים מופרדים בנקודתיים.
הפונקציה LayerLoader
עוברת בין השכבות לפי הסדר שציינתם, כך שהשכבה הראשונה נמצאת ישירות מתחת לאפליקציה. לכל שכבה, השדה LayerLoader
עוקב אחרי נקודות הכניסה AndroidGLESLayer_Initialize
ו-AndroidGLESLayer_GetProcAddress
. כדי שאפשר יהיה לטעון את השכבות, הן צריכות לספק את הממשקים האלה.
typedef void* (*PFNEGLGETNEXTLAYERPROCADDRESSPROC)(void*, const char*); void* AndroidGLESLayer_Initialize(void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address))
AndroidGLESLayer_Initialize()
מספק מזהה לשכבה לשימוש (layer_id
) ונקודת כניסה שאפשר להפעיל כדי לחפש פונקציות מתחת לשכבה. אפשר להשתמש בנקודת הכניסה כמו שמתואר בקטע הקוד לדוגמה הבא:
const char* func = "eglFoo"; void* gpa = get_next_layer_proc_address(layer_id, func);
AndroidGLESLayer_GetProcAddress
מקבלת את הכתובת של הקריאה הבאה בשרשרת שהשכבה צריכה לקרוא בסיום. אם יש רק שכבה אחת, הערך של next
מפנה ישירות לנהג ברוב הפונקציות.
typedef __eglMustCastToProperFunctionPointerType EGLFuncPointer; void* AndroidGLESLayer_GetProcAddress(const char *funcName, EGLFuncPointer next)
לכל שכבה ש-GLES LayerLoader
מוצא, הוא קורא ל-AndroidGLESLayer_Initialize
, עובר על רשימות הפונקציות של libEGL
וקורא ל-AndroidGLESLayer_GetProcAddress
לכל הפונקציות הידועות. השכבה קובעת איך לעקוב אחר הכתובת הבאה. אם השכבה מיירטת פונקציה, היא עוקבת אחרי הכתובת של הפונקציה. אם השכבה לא מפריעה לפונקציה, הפונקציה AndroidGLESLayer_GetProcAddress
מחזירה את אותה כתובת פונקציה שהועברה אליה. לאחר מכן, LayerLoader
מעדכן את רשימת ה-function hook כך שתצביע על נקודת הכניסה של השכבה.
השכבות לא נדרשות לעשות שום דבר עם המידע AndroidGLESLayer_Initialize
ו-get_next_layer_proc_address
, אבל מסירת הנתונים מקל על שכבות קיימות, כמו Android GPU Inspector ו-RenderDoc, כדי לתמוך ב-Android. בעזרת הנתונים האלה, שכבה יכולה לחפש פונקציות באופן עצמאי במקום להמתין לשיחות ל-AndroidGLESLayer_GetProcAddress
. אם השכבות בוחרות להפעיל את עצמן לפני שהמטען שולח שאילתה לכל נקודות הכניסה, הן צריכות להשתמש ב-get_next_layer_proc_address
. צריך להעביר את eglGetProcAddress
לאורך השרשרת אל הפלטפורמה.
מיקום שכבות
LayerLoader
של GLES מחפש שכבות במיקומים הבאים, לפי סדר עדיפות:
1. מיקום המערכת של root
לשם כך נדרשת הרשאת root
adb root adb disable-verity adb reboot adb root adb shell setenforce 0 adb shell mkdir -p /data/local/debug/gles adb push <layer>.so /data/local/debug/gles/
2. ספריית הבסיס של האפליקציה
אפליקציית היעד צריכה להיות ניתנת לניפוי באגים, או צריכה להיות לכם גישה לרמה הבסיסית (root):
adb push libGLTrace.so /data/local/tmp adb shell run-as com.android.gl2jni cp /data/local/tmp/libGLTrace.so . adb shell run-as com.android.gl2jni ls | grep libGLTrace libGLTrace.so
3. APK חיצוני
קובעים את ה-ABI של אפליקציית היעד ומתקינים קובץ APK שמכיל את השכבות שרוצים לטעון:
adb install --abi armeabi-v7a layers.apk
4. ב-APK של אפליקציית היעד
בדוגמה הבאה מוסבר איך למקם שכבות בקובץ ה-APK של האפליקציה:
$ jar tf GLES_layers.apk lib/arm64-v8a/libGLES_glesLayer1.so lib/arm64-v8a/libGLES_glesLayer2.so lib/arm64-v8a/libGLES_glesLayer3.so lib/armeabi-v7a/libGLES_glesLayer1.so lib/armeabi-v7a/libGLES_glesLayer2.so lib/armeabi-v7a/libGLES_glesLayer3.so resources.arsc AndroidManifest.xml META-INF/CERT.SF META-INF/CERT.RSA META-INF/MANIFEST.MF
הפעלת שכבות
אפשר להפעיל שכבות GLES לכל אפליקציה או באופן גלובלי. ההגדרות של כל אפליקציה נשארות ללא שינוי אחרי הפעלה מחדש, אבל הנכסים הגלובליים נמחקים אחרי הפעלה מחדש.
מודל האבטחה ומדיניות האבטחה של Android שונים באופן משמעותי מפלטפורמות אחרות. כדי לטעון שכבות חיצוניות, צריך להתקיים אחד מהתנאים הבאים:
קובץ המניפסט של אפליקציית היעד כולל את אלמנט המטא-נתונים הבא (רלוונטי רק לאפליקציות שמטרגטות את Android 11 (רמת API 30) ואילך):
<meta-data android:name="com.android.graphics.injectLayers.enable" android:value="true" />
כדאי להשתמש באפשרות הזו כדי ליצור פרופיל של האפליקציה.
אפשר לנפות באגים באפליקציית היעד. האפשרות הזו מספקת מידע נוסף על ניפוי הבאגים, אבל עשויה להשפיע לרעה על ביצועי האפליקציה.
אפליקציית היעד פועלת בגרסה של מערכת ההפעלה עם build של userdebug, שמעניקה הרשאת root.
כדי להפעיל שכבות לכל אפליקציה:
# Enable layers adb shell settings put global enable_gpu_debug_layers 1 # Specify target application adb shell settings put global gpu_debug_app <package_name> # Specify layer list (from top to bottom) # Layers are identified by their filenames, such as "libGLLayer.so" adb shell settings put global gpu_debug_layers_gles <layer1:layer2:layerN> # Specify packages to search for layers adb shell settings put global gpu_debug_layer_app <package1:package2:packageN>
כדי להשבית שכבות לכל אפליקציה:
# Delete the global setting that enables layers adb shell settings delete global enable_gpu_debug_layers # Delete the global setting that selects target application adb shell settings delete global gpu_debug_app # Delete the global setting that specifies layer list adb shell settings delete global gpu_debug_layers_gles # Delete the global setting that specifies layer packages adb shell settings delete global gpu_debug_layer_app
כדי להפעיל את השכבות באופן גלובלי:
# This attempts to load layers for all applications, including native # executables adb shell setprop debug.gles.layers <layer1:layer2:layerN>
יצירת שכבה
השכבות חייבות לחשוף את שתי הפונקציות הבאות שמתוארות בקטע הפעלת EGL Loader:
AndroidGLESLayer_Initialize AndroidGLESLayer_GetProcAddress
שכבות פסיביות
לשכבה שמיירטת רק מספר קטן של פונקציות, היא אופטימלית לשכבה שמופעלת באופן פסיבי. השכבה שמופעל בה איפוס פסיבי ממתינה ל-GLES LayerLoader
כדי להפעיל את הפונקציה שנדרשת לה.
דוגמת הקוד הבאה מראה איך ליצור שכבה פסיבית.
namespace { std::unordered_map<std::string, EGLFuncPointer> funcMap; EGLAPI EGLBoolean EGLAPIENTRY glesLayer_eglChooseConfig ( EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config) { EGLFuncPointer entry = funcMap["eglChooseConfig"]; typedef EGLBoolean (*PFNEGLCHOOSECONFIGPROC)( EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*); PFNEGLCHOOSECONFIGPROC next = reinterpret_cast<PFNEGLCHOOSECONFIGPROC>(entry); return next(dpy, attrib_list, configs, config_size, num_config); } EGLAPI EGLFuncPointer EGLAPIENTRY eglGPA(const char* funcName) { #define GETPROCADDR(func) if(!strcmp(funcName, #func)) { \ return (EGLFuncPointer)glesLayer_##func; } GETPROCADDR(eglChooseConfig); // Don't return anything for unrecognized functions return nullptr; } EGLAPI void EGLAPIENTRY glesLayer_InitializeLayer( void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) { // This function is purposefully empty, since this layer does not proactively // look up any entrypoints } EGLAPI EGLFuncPointer EGLAPIENTRY glesLayer_GetLayerProcAddress( const char* funcName, EGLFuncPointer next) { EGLFuncPointer entry = eglGPA(funcName); if (entry != nullptr) { funcMap[std::string(funcName)] = next; return entry; } return next; } } // namespace extern "C" { __attribute((visibility("default"))) EGLAPI void AndroidGLESLayer_Initialize( void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) { return (void)glesLayer_InitializeLayer(layer_id, get_next_layer_proc_address); } __attribute((visibility("default"))) EGLAPI void* AndroidGLESLayer_GetProcAddress( const char *funcName, EGLFuncPointer next) { return (void*)glesLayer_GetLayerProcAddress(funcName, next); } }
שכבות פעילות
שכבות רשמיות יותר שצריך לאתחל באופן מלא מראש, או שכבות שצריך לחפש בהן תוספים שלא מוכרים ל-EGL Loader, מחייבות אתחול של שכבה פעילה. השכבה משתמשת ברכיב get_next_layer_proc_address
שמסופק על ידי AndroidGLESLayer_Initialize
כדי לחפש פונקציה. השכבה עדיין צריכה להגיב לבקשות AndroidGLESLayer_GetProcAddress
מהמטען כדי שהפלטפורמה תדע לאן לנתב את הקריאות. דוגמת הקוד הבאה מראה איך ליצור שכבה פעילה.
namespace { std::unordered_map<std::string, EGLFuncPointer> funcMap; EGLAPI EGLBoolean EGLAPIENTRY glesLayer_eglChooseConfig ( EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config) { EGLFuncPointer entry = funcMap["eglChooseConfig"]; typedef EGLBoolean (*PFNEGLCHOOSECONFIGPROC)( EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*); PFNEGLCHOOSECONFIGPROC next = reinterpret_cast<PFNEGLCHOOSECONFIGPROC>(entry); return next(dpy, attrib_list, configs, config_size, num_config); } EGLAPI EGLFuncPointer EGLAPIENTRY eglGPA(const char* funcName) { #define GETPROCADDR(func) if(!strcmp(funcName, #func)) { \ return (EGLFuncPointer)glesLayer_##func; } GETPROCADDR(eglChooseConfig); // Don't return anything for unrecognized functions return nullptr; } EGLAPI void EGLAPIENTRY glesLayer_InitializeLayer( void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) { // Note: This is where the layer would populate its function map with all the // functions it cares about const char* func = “eglChooseConfig”; funcMap[func] = get_next_layer_proc_address(layer_id, func); } EGLAPI EGLFuncPointer EGLAPIENTRY glesLayer_GetLayerProcAddress( const char* funcName, EGLFuncPointer next) { EGLFuncPointer entry = eglGPA(funcName); if (entry != nullptr) { return entry; } return next; } } // namespace extern "C" { __attribute((visibility("default"))) EGLAPI void AndroidGLESLayer_Initialize( void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) { return (void)glesLayer_InitializeLayer(layer_id, get_next_layer_proc_address); } __attribute((visibility("default"))) EGLAPI void* AndroidGLESLayer_GetProcAddress( const char *funcName, EGLFuncPointer next) { return (void*)glesLayer_GetLayerProcAddress(funcName, next); } }