Так случилось, что собраное мною ядро для китайфона (MSM8228) при тестировании заставляло оного переходить в EDL (Emergency Download) режим и выяснить причину такого поведения было абсолютно невозможно без логов ядра. Поэтому поставил себе цель: прочитать эти самые логи для понимания причин падений.
Конечно на ум сразу приходят два варианта: заставить ядро выводить логи на экран или при падении скидывать логи на eMMC устройство. Но оба эти варианты сложны, да и не предоставляют возможности получения логов на ранних этапах инициализации ядра. Остается только логирование в область памяти, которая бы не изменилась при перезагрузках системы. Как оказалось такая область памяти называется persistent ram (pram) и инициализируется в самом начале работы ядра. В данном случае под инициализацией понимается просто напросто резервирование участка памяти для особых целей.
Участок persistent ram обычно используется для работы устройства ram_console, которое нужно для формирование файла "/proc/last_kmsg" (и не только). Т.е. это устройство при падении ядра занимается созданием копии логов в области persistent ram, а при загрузке ядра создает файл last_kmsg. Наличие такого файла позволяет узнать о причинах падения ядра. Но если падение возникает на этапе инициализации подсистем ядра, то этот механизм бесполезен.
Физич адрес и размер persistent ram можно узнать при просмотре логов стокового ядра.
В ядро, которое используется в моем китайфоне, производитель просто напросто поленился добавить поддержку persistent ram и ram_console. Поэтому сперва мне нужно было выяснить физический адрес и размер pram. Как оказалось это довольно просто, т.к. при поиске информации на глаза попался вот такой кодес:
Далее нужно было провести эксперимент, который показывал бы наглядно, что в этой области памяти (0x7f100000 ... 0x7f2fffff) данные не затираются после перезагрузки. Для этого пришлось стоковый образ boot.img разобрать на составляющие части. Затем пришлось декодировать файл dtb.img в DTS-файл (в предыдущей статье я об этом уже писал).
В результате получил файлик phone.dts , который описывает "внутренее строение" девайса (Flattened Device Tree). Открыл этот файлик на редактирование и добавил 1 строчку в самое начало:
Далее скомпилировал phone.dts в dtb.img и при помощи утилиты AndImgTool собрал новый образ boot.img , в котором QCDT-структура имеет информацию о persistent ram.
При помощи команды "fastboot flash boot boot.img" обновил в девайсе ядро. После загрузки андройда установил утилиту devmem2 и выполнил команды:
Стоит отметить, что при возникновении критической ошибки в ядре моё устройство перезагружается в EDL режим. Выход из этого режима возможен только при использовании кнопки Power. Но мои эксперименты показали, что использование Power в большинстве случаев влияет на содержимое памяти. Т.е. очень часто за место числа 0x51525354 считывалось число 0x8A805354 (видоизменялись байтики по адресам кратным 2 и 3). Видимо на разных устройствах будет как то иначе, но данное обстоятельство нужно обязательно учитывать.
Учитывая сказанное выше, можно составить список требований, при которых возможно прочесть логи при падении ядра:
1) обеспечить в тестируемом ядре резервирование 1МБ физической памяти (pram) по адресу 0x7f200000;
2) обеспечить в тестируемом ядре копирование логов из внутреннего буфера в зарезервированную область pram;
3) при падении тестируемого ядра обеспечить возможность загрузки стокового ядра без использования кнопки Power;
4) обеспечить в стоковом ядре резервирование 1МБ физической памяти по адресу 0x7f200000;
5) при загрузке стокового ядра обеспечить возможность считывания данных из SDRAM-памяти в файл.
Требование #1 реализуется добавлением в файл "/arch/arm/boot/dts/msm8xxx.dts" строчки "/memreserve/ 0x7f200000 0x100000".
Требование #2 реализуется добавлением в файлы printk.c и main.c нового функционала.
Сначала откроем файл "/kernel/printk.c".
В список хидеров нужно добавить ещё парочку:
Функция emit_log_char_pmem встраивается в функцию emit_log_char и занимается наполнением нового буфера по адресу 0x7f200000.
Новая опция "pmem_log=1" включает функционал копирования лога в новый буфер.
Новая опция "log_no_ring=1" заставляет ядро остановить логирование при достижении конца внутреннего буфера (это бывает очень полезно). Поэтому при использовании этой опции так же следует указать "log_buf_len=512K", что заставит ядро выделить под лог 512КБ памяти.
Теперь откроем файл "/include/linux/printk.h" и в самый конец добавим пару строчек:
Теперь откроем файл "/kernel/main.c". Находим функцию do_basic_setup и в самое начало этой функции вставляем вызовы новых функций:
Требование #3 реализуется внесением изменений в файл "/arch/arm/mach-msm/restart.c".
Сначала находим определение переменной "download_mode" и изменяем его значение на "0":
Требование #4 лучше выполнять над TeamWin recovery (TWRP). В результате получится удобное средство для снятия ядерных логов, т.к. TWRP предоставляет терминал и файловый менеджер. Так вот при помощи утилиты AndImgTool нужно образ TWRP разобрать на составляющие части. При помощи утилит dtc и dtbToolCM в файл dtb.img нужно добавить строчку "/memreserve/ 0x7f200000 0x100000". Но разобранный образ TWRP пока не собираем.
Требование #5 реализуется добавлением в TWRP утилиты viewmem (скачать viewmem). Добавлять файлик viewmem нужно в ramdisk в директорию sbin. Вот теперь можно при помощи AndImgTool собрать дополненный TWRP. При этом не забудьте в cpiostatfile.txt добавить информацию по новому файлику viewmem:
Сборка прошивки.
В файле BoardConfig.mk найдите параметр BOARD_KERNEL_CMDLINE и добавьте к списку параметров новые: "log_buf_len=512K log_no_ring=1 pmem_log=1 panic_restart=2". Теперь нужно собрать ядро (команда "mka bootimage") и положить в свою заранее собранную прошивку, которая вызывает сбой системы.
Тестирование прошивки.
Через fastboot прошиваем новый TWRP, через который заливаем в девайс свою прошивку. Запускаем прошивку. Если возникнет критическая ошибка (panic), то произойдет рестарт, при котором будет автоматически запущен TWRP. При входе в TWRP запускаем терминал и выполняем команды:
Все вышесказанное применимо для платформы Qualcomm MSM8xxx.
Конечно на ум сразу приходят два варианта: заставить ядро выводить логи на экран или при падении скидывать логи на eMMC устройство. Но оба эти варианты сложны, да и не предоставляют возможности получения логов на ранних этапах инициализации ядра. Остается только логирование в область памяти, которая бы не изменилась при перезагрузках системы. Как оказалось такая область памяти называется persistent ram (pram) и инициализируется в самом начале работы ядра. В данном случае под инициализацией понимается просто напросто резервирование участка памяти для особых целей.
Участок persistent ram обычно используется для работы устройства ram_console, которое нужно для формирование файла "/proc/last_kmsg" (и не только). Т.е. это устройство при падении ядра занимается созданием копии логов в области persistent ram, а при загрузке ядра создает файл last_kmsg. Наличие такого файла позволяет узнать о причинах падения ядра. Но если падение возникает на этапе инициализации подсистем ядра, то этот механизм бесполезен.
Физич адрес и размер persistent ram можно узнать при просмотре логов стокового ядра.
В ядро, которое используется в моем китайфоне, производитель просто напросто поленился добавить поддержку persistent ram и ram_console. Поэтому сперва мне нужно было выяснить физический адрес и размер pram. Как оказалось это довольно просто, т.к. при поиске информации на глаза попался вот такой кодес:
void __init lge_add_persist_ram_devices(void) { int ret; struct memtype_reserve *mt = &reserve_info->memtype_reserve_table[MEMTYPE_EBI1]; lge_persist_ram.start = mt->start - LGE_PERSISTENT_RAM_SIZE; // SZ_256K pr_info("PERSIST RAM CONSOLE START ADDR : 0x%x\n", lge_persist_ram.start); ret = persistent_ram_early_init(&lge_persist_ram); if (ret) pr_err("%s: failed to initialize persistent ram\n", __func__); }Если взглянуть на ядерные логи стоковой прошивки, то можно определить адрес EBI области памяти. В моем случае адрес начала EBI = 0x7f300000. Обычно под область pram выделяют не более 2МБ (0x200000 байт). Поэтому в качестве начала области pmem я выбрал адрес, 0x7f100000 (в моем китайфоне установлено 2ГБ SDRAM).
Далее нужно было провести эксперимент, который показывал бы наглядно, что в этой области памяти (0x7f100000 ... 0x7f2fffff) данные не затираются после перезагрузки. Для этого пришлось стоковый образ boot.img разобрать на составляющие части. Затем пришлось декодировать файл dtb.img в DTS-файл (в предыдущей статье я об этом уже писал).
В результате получил файлик phone.dts , который описывает "внутренее строение" девайса (Flattened Device Tree). Открыл этот файлик на редактирование и добавил 1 строчку в самое начало:
/dts-v1/; /memreserve/ 0x7f200000 0x100000; / { #address-cells = <0x1>; #size-cells = <0x1>; model = "Qualcomm MSM 8226v2 QRD"; compatible = "qcom,msm8226-qrd", "qcom,msm8226", "qcom,qrd";Команда memreserve заставляет ядро на самом раннем этапе зарезервировать указанную область физической памяти (первый мегабайт по адресу 0x7f100000 оставил для иных целей).
Далее скомпилировал phone.dts в dtb.img и при помощи утилиты AndImgTool собрал новый образ boot.img , в котором QCDT-структура имеет информацию о persistent ram.
При помощи команды "fastboot flash boot boot.img" обновил в девайсе ядро. После загрузки андройда установил утилиту devmem2 и выполнил команды:
$ su $ ./devmem2 0x7f200000 w 0x51525354Затем перезагрузил девайс и снова выполнил такие же команды. В консольном выводе увидел, что по адресу 0x7f200000 находилось значение 0x51525354. Значит данную область памяти можно смело использовать в качестве хранилища информации между различными состояниями девайса.
Стоит отметить, что при возникновении критической ошибки в ядре моё устройство перезагружается в EDL режим. Выход из этого режима возможен только при использовании кнопки Power. Но мои эксперименты показали, что использование Power в большинстве случаев влияет на содержимое памяти. Т.е. очень часто за место числа 0x51525354 считывалось число 0x8A805354 (видоизменялись байтики по адресам кратным 2 и 3). Видимо на разных устройствах будет как то иначе, но данное обстоятельство нужно обязательно учитывать.
Учитывая сказанное выше, можно составить список требований, при которых возможно прочесть логи при падении ядра:
1) обеспечить в тестируемом ядре резервирование 1МБ физической памяти (pram) по адресу 0x7f200000;
2) обеспечить в тестируемом ядре копирование логов из внутреннего буфера в зарезервированную область pram;
3) при падении тестируемого ядра обеспечить возможность загрузки стокового ядра без использования кнопки Power;
4) обеспечить в стоковом ядре резервирование 1МБ физической памяти по адресу 0x7f200000;
5) при загрузке стокового ядра обеспечить возможность считывания данных из SDRAM-памяти в файл.
Требование #1 реализуется добавлением в файл "/arch/arm/boot/dts/msm8xxx.dts" строчки "/memreserve/ 0x7f200000 0x100000".
Требование #2 реализуется добавлением в файлы printk.c и main.c нового функционала.
Сначала откроем файл "/kernel/printk.c".
В список хидеров нужно добавить ещё парочку:
#include <linux/io.h> #include <linux/vmalloc.h>Нужно найти функцию emit_log_char и сделать следующие исправления (выделено цветом):
static int log_no_ring = 0; static int __init log_no_ring_setup(char *str) { get_option(&str, &log_no_ring); return 0; } early_param("log_no_ring", log_no_ring_setup); static void emit_log_char_pmem(char c); static void emit_log_char(char c) { static int is_begin; int start_apanic_threads; start_apanic_threads = is_apanic_threads_dump(); if (unlikely(start_apanic_threads) && !is_begin) { is_begin = 1; log_end = 0; logged_chars = 0; } if (log_no_ring && log_end - log_start >= log_buf_len) goto exit; emit_log_char_pmem(c); LOG_BUF(log_end) = c; log_end++; if (log_end - log_start > log_buf_len) log_start = log_end - log_buf_len; if (log_end - con_start > log_buf_len) con_start = log_end - log_buf_len; if (logged_chars < log_buf_len) logged_chars++; exit: if (unlikely(start_apanic_threads && ((log_end & (LOG_BUF_MASK + 1)) == __LOG_BUF_LEN))) { emergency_dump(); is_begin = 0; } }Затем в самый конец файла "/kernel/printk.c" следует добавить:
#define PMEM_LOG_ADDR (0x7f200000) #define PMEM_LOG_SIZE (0x100000) #define PMEM_LOG_PAGES (PMEM_LOG_SIZE / PAGE_SIZE) static int pmem_log = 0; static void * pmem_virt = NULL; static uint32_t pmem_log_size = 0; static int pmem_log_beg = 0; static int __init pmem_log_setup(char *str) { get_option(&str, &pmem_log); return 0; } early_param("pmemlog", pmem_log_setup); int pmem_log_init(void) { uint32_t x; pgprot_t prot; struct page **pages; phys_addr_t addr; if (!pmem_log) retrun 0; if (pmem_virt) return 1; prot = pgprot_noncached(PAGE_KERNEL); pages = kmalloc(sizeof(struct page *) * PMEM_LOG_PAGES, GFP_KERNEL); if (!pages) return 0; for (x = 0; x < PMEM_LOG_PAGES; x++) { addr = (phys_addr_t)PMEM_LOG_ADDR + x * PAGE_SIZE; pages[x] = pfn_to_page(addr >> PAGE_SHIFT); } pmem_virt = vmap(pages, PMEM_LOG_PAGES, VM_MAP, prot); kfree(pages); if (!pmem_virt) return 0; for (x = 0; x < PMEM_LOG_SIZE - PAGE_SIZE - 16; x += 4) { __raw_writel_no_log(0, (uint32_t)pmem_virt + x); } return 1; } int pmem_log_start(void) { uint32_t x; if (!pmem_log) retrun 0; if (!pmem_virt) return 0; pmem_log_beg = 1; if (logged_chars < PMEM_LOG_SIZE - 100) { for (x = 0; x < logged_chars; x++) { __raw_writeb_no_log(log_buf[x], (uint32_t)pmem_virt + x); } __raw_writeb_no_log('\n', (uint32_t)pmem_virt + logged_chars); pmem_log_size = logged_chars + 1; } return 1; } static void emit_log_char_pmem(char c) { if (pmem_log_beg && pmem_virt && pmem_log_size < PMEM_LOG_SIZE-16) { __raw_writeb_no_log(c, (uint32_t)pmem_virt + pmem_log_size); pmem_log_size++; } }Функция pmem_log_init инициализирует pram память под копию ядерного лога.
Функция emit_log_char_pmem встраивается в функцию emit_log_char и занимается наполнением нового буфера по адресу 0x7f200000.
Новая опция "pmem_log=1" включает функционал копирования лога в новый буфер.
Новая опция "log_no_ring=1" заставляет ядро остановить логирование при достижении конца внутреннего буфера (это бывает очень полезно). Поэтому при использовании этой опции так же следует указать "log_buf_len=512K", что заставит ядро выделить под лог 512КБ памяти.
Теперь откроем файл "/include/linux/printk.h" и в самый конец добавим пару строчек:
int pmem_log_init(void); int pmem_log_start(void); #endif
Теперь откроем файл "/kernel/main.c". Находим функцию do_basic_setup и в самое начало этой функции вставляем вызовы новых функций:
static void __init do_basic_setup(void) { pmem_log_init(); pmem_log_start(); cpuset_init_smp(); usermodehelper_init(); shmem_init(); driver_init(); init_irq_proc(); do_ctors(); usermodehelper_enable(); do_initcalls(); }Вызов этих двух функций заставил ядро дублировать логи в нашу область pram.
Требование #3 реализуется внесением изменений в файл "/arch/arm/mach-msm/restart.c".
Сначала находим определение переменной "download_mode" и изменяем его значение на "0":
/* Download mode master kill-switch */ static int dload_set(const char *val, struct kernel_param *kp); static int download_mode = 0; module_param_call(download_mode, dload_set, param_get_int, &download_mode, 0644);Затем находим функцию msm_restart_prepare и изменяем код:
#define SOFTWARE_RESET 0x73727374 #define FASTBOOT_MODE 0x77665500 #define ADB_REBOOT 0x77665501 #define RECOVERY_MODE 0x77665502 #define UARTLOG_MODE 0x77665503 #define NORMAL_BOOT 0x77665504 #define APANIC_REBOOT 0x77665505 static int panic_restart = 1; // default ADB_REBOOT static int __init panic_restart_setup(char *str) { get_option(&str, &panic_restart); return 0; } early_param("panic_restart", panic_restart_setup); static void msm_restart_prepare(const char *cmd) { #ifdef CONFIG_MSM_DLOAD_MODE /* This looks like a normal reboot at this point. */ set_dload_mode(0); /* Write download mode flags if we're panic'ing */ set_dload_mode(in_panic); /* Write download mode flags if restart_mode says so */ if (restart_mode == RESTART_DLOAD) set_dload_mode(1); /* Kill download mode if master-kill switch is set */ if (!download_mode) set_dload_mode(0); #endif pm8xxx_reset_pwr_off(1); /* Hard reset the PMIC unless memory contents must be maintained. */ if (get_dload_mode() || (cmd != NULL && cmd[0] != '\0') || in_panic) qpnp_pon_system_pwr_off(PON_POWER_OFF_WARM_RESET); else qpnp_pon_system_pwr_off(PON_POWER_OFF_HARD_RESET); if (cmd != NULL) { if (!strncmp(cmd, "bootloader", 10)) { __raw_writel(0x77665500, restart_reason); } else if (!strncmp(cmd, "recovery", 8)) { __raw_writel(0x77665502, restart_reason); } else if (!strcmp(cmd, "rtc")) { __raw_writel(0x77665503, restart_reason); } else if (!strncmp(cmd, "oem-", 4)) { unsigned long code; code = simple_strtoul(cmd + 4, NULL, 16) & 0xff; __raw_writel(0x6f656d00 | code, restart_reason); } else if (!strncmp(cmd, "edl", 3)) { enable_emergency_dload_mode(); } else { __raw_writel(FASTBOOT_MODE + panic_restart, restart_reason); } } else { __raw_writel(FASTBOOT_MODE + panic_restart, restart_reason); } flush_cache_all(); outer_flush_all(); }Новая опция "panic_restart=2" заставит ядро при рестарте системы всегда запускаться с recovery раздела (точнее даёт команду бутлоадеру на запуск recovery). Т.е. даже при возникновении критической ошибки (panic) будет осуществлён переход в recovery режим.
Требование #4 лучше выполнять над TeamWin recovery (TWRP). В результате получится удобное средство для снятия ядерных логов, т.к. TWRP предоставляет терминал и файловый менеджер. Так вот при помощи утилиты AndImgTool нужно образ TWRP разобрать на составляющие части. При помощи утилит dtc и dtbToolCM в файл dtb.img нужно добавить строчку "/memreserve/ 0x7f200000 0x100000". Но разобранный образ TWRP пока не собираем.
Требование #5 реализуется добавлением в TWRP утилиты viewmem (скачать viewmem). Добавлять файлик viewmem нужно в ramdisk в директорию sbin. Вот теперь можно при помощи AndImgTool собрать дополненный TWRP. При этом не забудьте в cpiostatfile.txt добавить информацию по новому файлику viewmem:
"twrp_/ramdisk/sbin/libdl.so",32768,1000,1000,750 "twrp_/ramdisk/sbin/mpstat",40960,1000,1000,777 "twrp_/ramdisk/sbin/toolbox",32768,1000,1000,750 "twrp_/ramdisk/sbin/viewmem",32768,1000,1000,777 "twrp_/ramdisk/sbin/libstdc++.so",32768,1000,1000,750
Сборка прошивки.
В файле BoardConfig.mk найдите параметр BOARD_KERNEL_CMDLINE и добавьте к списку параметров новые: "log_buf_len=512K log_no_ring=1 pmem_log=1 panic_restart=2". Теперь нужно собрать ядро (команда "mka bootimage") и положить в свою заранее собранную прошивку, которая вызывает сбой системы.
Тестирование прошивки.
Через fastboot прошиваем новый TWRP, через который заливаем в девайс свою прошивку. Запускаем прошивку. Если возникнет критическая ошибка (panic), то произойдет рестарт, при котором будет автоматически запущен TWRP. При входе в TWRP запускаем терминал и выполняем команды:
$ su $ viewmem 0x7f200000 0x100000 >/data/kmsg.txtЗатем при помощи файлового менеджера копируем файл "/data/kmsg.txt" на sdcard. После этого можно заняться изучением логов и исправлением багов.
Все вышесказанное применимо для платформы Qualcomm MSM8xxx.
Комментариев нет:
Отправить комментарий