במכשירים עם Android 10 (רמת API 29) ומעלה, אפשר להשתמש בשכבות של OpenGL ES (GLES). אפליקציה שניתן לבצע בה ניפוי באגים יכולה לטעון שכבות GLES מה-APK שלה, מספריית הבסיס שלה או מ-APK של שכבה שנבחרה.
השימוש בשכבת GLES דומה לשימוש בשכבת האימות של Vulkan.
דרישות
שכבות GLES נתמכות רק בגרסאות GLES 2.0 ואילך.
אתחול השכבה
אחרי אכלוס נקודות הכניסה הרגילות, ה-EGL Loader יוצר מופע של GLES
LayerLoader. אם שכבות ניפוי הבאגים מופעלות, 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 מעדכן את רשימת ה-hook של הפונקציות כך שתצביע על נקודת הכניסה של השכבה.
השכבות לא נדרשות לעשות שום דבר עם המידע ש-AndroidGLESLayer_Initialize ו-get_next_layer_proc_address מספקים, אבל הנתונים האלה מקלים על שכבות קיימות, כמו Android GPU Inspector ו-RenderDoc, לתמוך ב-Android. בעזרת הנתונים האלה, שכבה יכולה לחפש פונקציות באופן עצמאי במקום להמתין לקריאות ל-AndroidGLESLayer_GetProcAddress. אם השכבות בוחרות לאתחל את עצמן לפני שהטוען שולח שאילתות לכל נקודות הכניסה, הן צריכות להשתמש ב-get_next_layer_proc_address. צריך להעביר את eglGetProcAddress במורד השרשרת אל הפלטפורמה.
שכבות של מקומות
GLES LayerLoader מחפש שכבות במיקומים הבאים, לפי סדר העדיפות:
1. מיקום המערכת של הבסיס
לשם כך נדרשת גישת 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 של מערכת ההפעלה לניפוי באגים של המשתמש, שמעניקה גישת בסיס.
כדי להפעיל שכבות לכל אפליקציה:
# 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); } }