Category:Bootloaders/Bootloaders porting using linux
About
This article describes how to access the debug UART, when you don't have vendor's debug UART cable and don't want to disassemble your phone, and how to execute a payload (i.e. your bootloader port).
You must have access to debug UART, if you want to port a bootloader to your phone. However, some bootloaders and/or MUIC chips do not allow you to connect to debug UART (like recent Samsung phones with USB Type-C). In this case, you can either:
- disassemble the phone, find and connect to exposed debug uart pins on the board(the location of these pins and their accessibility varies from board to board), and use that to port your bootloader.
- Hack linux to forcefully expose debug UART, and then test your bootloader.
We'll speak about the latter.
This method is quite similar to kexec, though differs in some details:
- MMU should be turned off completely
- Use bin format, to simplify load process.
Getting started
You'll need:
- a pmOS downstream port of your device (alternatively, TWRP can be used)
- Stuff for building a debug cable
Getting UART debug output from linux
Hacking the MUIC driver to expose UART on request
We can use a sysfs attribute to tell the MUIC driver to switch debug UART to USB when a certain event occurs(for example, when the user excutes a certain hardware button/key sequence). As an example, here is the MUIC driver patch for Samsung Galaxy S9
diff --git a/drivers/muic/max77705-muic.c b/drivers/muic/max77705-muic.c
index c424c03f9..156152daf 100644
--- a/drivers/muic/max77705-muic.c
+++ b/drivers/muic/max77705-muic.c
@@ -5,6 +5,7 @@
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
+#define MANUAL_UART_SWITCH
#include <linux/kernel.h>
#include <linux/module.h>
@@ -1005,8 +1006,81 @@ static ssize_t hiccup_store(struct device *dev,
}
#endif /* CONFIG_HICCUP_CHARGER */
+void max77705_muic_read_register(struct i2c_client *i2c)
+{
+ const enum max77705_usbc_reg regfile[] = {
+ MAX77705_USBC_REG_UIC_HW_REV,
+ MAX77705_USBC_REG_USBC_STATUS1,
+ MAX77705_USBC_REG_USBC_STATUS2,
+ MAX77705_USBC_REG_BC_STATUS,
+ MAX77705_USBC_REG_UIC_INT_M,
+ };
+ u8 val;
+ int i, ret;
+
+ pr_info("%s read register--------------\n", __func__);
+ for (i = 0; i < (int)ARRAY_SIZE(regfile); i++) {
+ ret = max77705_muic_read_reg(i2c, regfile[i], &val);
+ if (ret) {
+ pr_err("%s:%s fail to read muic reg(0x%02x), ret=%d\n",
+ MUIC_DEV_NAME, __func__, regfile[i], ret);
+ continue;
+ }
+
+ pr_info("%s:%s reg(0x%02x)=[0x%02x]\n", MUIC_DEV_NAME, __func__,
+ regfile[i], val);
+ }
+ pr_info("%s:%s end register---------------\n", MUIC_DEV_NAME, __func__);
+}
+
+static int max77705_muic_attach_uart_path(struct max77705_muic_data *muic_data,
+ muic_attached_dev_t new_dev)
+{
+ struct muic_platform_data *pdata = muic_data->pdata;
+ int ret = 0;
+
+ pr_info("%s:%s\n", MUIC_DEV_NAME, __func__);
+
+ if (pdata->uart_path == MUIC_PATH_UART_AP)
+ ret = switch_to_ap_uart(muic_data, new_dev);
+ else if (pdata->uart_path == MUIC_PATH_UART_CP)
+ ret = switch_to_cp_uart(muic_data, new_dev);
+ else
+ pr_warn("%s:%s invalid uart_path\n", MUIC_DEV_NAME, __func__);
+
+ return ret;
+}
+
+#ifdef MANUAL_UART_SWITCH
+static ssize_t max77705_muic_switch_uart(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct max77705_muic_data *muic_data = dev_get_drvdata(dev);
+ struct muic_platform_data *pdata = muic_data->pdata;
+
+ mutex_lock(&muic_data->muic_mutex);
+
+ if (!strncasecmp(buf, "1", 1)) {
+ max77705_muic_attach_uart_path(muic_data, ATTACHED_DEV_JIG_UART_OFF_MUIC);
+ } else if (!strncasecmp(buf, "0", 1)) {
+ com_to_open(muic_data);
+ } else {
+ pr_warn("Value should be 1 for on, 0 for off\n");
+ }
+
+ mutex_unlock(&muic_data->muic_mutex);
+
+ return count;
+}
+#endif
+
static DEVICE_ATTR(uart_sel, 0664, max77705_muic_show_uart_sel,
max77705_muic_set_uart_sel);
+#ifdef MANUAL_UART_SWITCH
+static DEVICE_ATTR(muic_switch_uart, 0220, NULL,
+ max77705_muic_switch_uart);
+#endif
static DEVICE_ATTR(usb_sel, 0664, max77705_muic_show_usb_sel,
max77705_muic_set_usb_sel);
static DEVICE_ATTR(uart_en, 0660, max77705_muic_show_uart_en,
@@ -1038,6 +1112,9 @@ static struct attribute *max77705_muic_attributes[] = {
&dev_attr_attached_dev.attr,
&dev_attr_otg_test.attr,
&dev_attr_apo_factory.attr,
+ #ifdef MANUAL_UART_SWITCH
+ &dev_attr_muic_switch_uart.attr,
+ #endif
#if defined(CONFIG_HV_MUIC_MAX77705_AFC)
&dev_attr_afc_disable.attr,
#endif /* CONFIG_HV_MUIC_MAX77705_AFC */
@@ -1054,51 +1131,6 @@ static const struct attribute_group max77705_muic_group = {
.attrs = max77705_muic_attributes,
};
-void max77705_muic_read_register(struct i2c_client *i2c)
-{
- const enum max77705_usbc_reg regfile[] = {
- MAX77705_USBC_REG_UIC_HW_REV,
- MAX77705_USBC_REG_USBC_STATUS1,
- MAX77705_USBC_REG_USBC_STATUS2,
- MAX77705_USBC_REG_BC_STATUS,
- MAX77705_USBC_REG_UIC_INT_M,
- };
- u8 val;
- int i, ret;
-
- pr_info("%s read register--------------\n", __func__);
- for (i = 0; i < (int)ARRAY_SIZE(regfile); i++) {
- ret = max77705_muic_read_reg(i2c, regfile[i], &val);
- if (ret) {
- pr_err("%s:%s fail to read muic reg(0x%02x), ret=%d\n",
- MUIC_DEV_NAME, __func__, regfile[i], ret);
- continue;
- }
-
- pr_info("%s:%s reg(0x%02x)=[0x%02x]\n", MUIC_DEV_NAME, __func__,
- regfile[i], val);
- }
- pr_info("%s:%s end register---------------\n", MUIC_DEV_NAME, __func__);
-}
-
-static int max77705_muic_attach_uart_path(struct max77705_muic_data *muic_data,
- muic_attached_dev_t new_dev)
-{
- struct muic_platform_data *pdata = muic_data->pdata;
- int ret = 0;
-
- pr_info("%s:%s\n", MUIC_DEV_NAME, __func__);
-
- if (pdata->uart_path == MUIC_PATH_UART_AP)
- ret = switch_to_ap_uart(muic_data, new_dev);
- else if (pdata->uart_path == MUIC_PATH_UART_CP)
- ret = switch_to_cp_uart(muic_data, new_dev);
- else
- pr_warn("%s:%s invalid uart_path\n", MUIC_DEV_NAME, __func__);
-
- return ret;
-}
-
static int max77705_muic_attach_usb_path(struct max77705_muic_data *muic_data,
muic_attached_dev_t new_dev)
{
This patch will add a new sysfs attribute, dev_attr_muic_switch_uart.attr
, so that the MUIC driver will attach UART upon writing a value of '1' to that attribute, and detach it when a value of '0' is written to it.
Setup pmOS to switch MUIC on key combination
Use triggerhappy
Building a debug cable
- First of all, you should determine on what pins MUIC multiplexes UART, i.e., you have to find the TX and RX pins on USB.
The process is similar to finding TX RX pins on the PCB For USB Type-C devices it will likely be either the D+D- or the SBU pins.
- assemble the cable
- ensure you get an output from your cable, when writing to the corresponding tty on the phone
Hacking the kernel to make it execute your bootloader
- enable
CONFIG_DEVMEM
- disable the watchdog timer (it forces a reboot, if the kernel doesn't respond)
The simplest way to do this is to delete the corresponding device tree node, for qcom devices this is usually compatible = "qcom,msm-watchdog";
- we then apply a patch to launch the bootloader from linux
diff --git a/arch/arm64/kernel/cpu-reset.S b/arch/arm64/kernel/cpu-reset.S
index 65f42d257..86c26955a 100644
--- a/arch/arm64/kernel/cpu-reset.S
+++ b/arch/arm64/kernel/cpu-reset.S
@@ -34,16 +34,15 @@
*/
ENTRY(__cpu_soft_restart)
/* Clear sctlr_el1 flags. */
- mrs x12, sctlr_el1
- ldr x13, =SCTLR_ELx_FLAGS
- bic x12, x12, x13
- msr sctlr_el1, x12
+ /* Turn off MMU. Note, we can do that only from .idmap.text section,
+ * when id mapping installed. For details on MMU shutdown/bring up, see
+ *
+ * https://www.programmersought.com/article/82334578227/
+ * https://stackoverflow.com/questions/30843270/arm-disabling-mmu-and-updating-pc
+ */
+ msr sctlr_el1, xzr
isb
- cbz x0, 1f // el2_switch?
- mov x0, #HVC_SOFT_RESTART
- hvc #0 // no return
-
1: mov x18, x1 // entry
mov x0, x2 // arg0
mov x1, x3 // arg1
diff --git a/drivers/Makefile b/drivers/Makefile
index c571b2694..f3bbe0729 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -5,6 +5,8 @@
# Rewritten to use lists instead of if-statements.
#
+obj-y += jump/
+
obj-y += irqchip/
obj-y += bus/
diff --git a/drivers/jump/Makefile b/drivers/jump/Makefile
new file mode 100644
index 000000000..38a3b48cc
--- /dev/null
+++ b/drivers/jump/Makefile
@@ -0,0 +1 @@
+obj-y += jump.o
diff --git a/drivers/jump/jump.c b/drivers/jump/jump.c
new file mode 100644
index 000000000..bbadf123c
--- /dev/null
+++ b/drivers/jump/jump.c
@@ -0,0 +1,114 @@
+/*
+ * Author: Dzmitry Sankouski <dsankouski@gmail.com>
+ * License terms: GNU General Public License (GPL), version 2
+ *
+ * Based on kexec code.
+ * Usage:
+ * - read jump_code_buffer physical address from sysfs
+ * - write target code to jump_code_buffer region
+ * - perform write to `jump` file to shutdown linux, and run your code
+ */
+
+#include <asm/mmu_context.h>
+#include <linux/cpu.h>
+#include <linux/err.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/kobject.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/platform_device.h>
+#include <linux/reboot.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include "../../arch/arm64/kernel/cpu-reset.h"
+
+void *jump_code_buffer;
+
+static ssize_t jump(struct kobject *kobj, struct kobj_attribute *attr,
+ const char *buf, size_t count) {
+ unsigned long jump_address;
+ jump_address = __pa(jump_code_buffer);
+ pr_info("Jump to address 0x%p\n", jump_address);
+
+ kernel_restart_prepare(NULL);
+ pr_info("Kernel restart prepared\n");
+ migrate_to_reboot_cpu();
+ pr_info("Migrated to reboot cpu\n");
+ /*
+ * migrate_to_reboot_cpu() disables CPU hotplug assuming that
+ * no further code needs to use CPU hotplug (which is true in
+ * the reboot case). However, the kexec path depends on using
+ * CPU hotplug again; so re-enable it here.
+ */
+ /* Do I really need this? */
+ cpu_hotplug_enable();
+ pr_info("Cpu hotplug enabled\n");
+ machine_shutdown();
+
+ pr_info("Machine shutdown\n");
+ /* Disable all DAIF exceptions. */
+ asm volatile("msr daifset, #0xf" : : : "memory");
+ pr_info("DAIF exceptions disabled\n");
+ pr_info("Bye!\n");
+ /*
+ * cpu_soft_restart will shutdown the MMU, disable data caches, then
+ * transfer control to the jump_code_buffer which contains user - loaded
+ * code
+ *
+ */
+ cpu_soft_restart(1, jump_address, 0, 0, 0);
+ /* unreachable */
+ return count;
+}
+
+static ssize_t show_jump_address(struct kobject *k, struct kobj_attribute *attr,
+ char *buf) {
+ unsigned long jump_address;
+ jump_address = __pa(jump_code_buffer);
+
+ return sprintf(buf, "0x%p\n", (void *) jump_address);
+}
+
+static struct kobj_attribute jump_attr = __ATTR(jump, 0220, NULL, jump);
+static struct kobj_attribute jump_address =
+ __ATTR(show_jump_address, 0440, show_jump_address, NULL);
+
+static struct attribute *jump_attributes[] = {
+ &jump_attr.attr,
+ &jump_address.attr,
+ NULL
+};
+
+static const struct attribute_group jump_group = {
+ .attrs = jump_attributes,
+};
+
+struct kobject *module_kobj;
+
+static int __init init_events_group(void) {
+ pr_info("Allocate memory for target code buffer\n");
+ jump_code_buffer = kzalloc(0x100000, GFP_KERNEL);
+ pr_info("reboot code buffer phys address: 0x%p \n", __pa(jump_code_buffer));
+ pr_info("Create sysfs files for jump\n");
+ int ret;
+
+ module_kobj = kobject_create_and_add("jump", kernel_kobj);
+ if (!module_kobj) {
+ pr_err("jump: Couldn't create module kobject\n");
+ return -ENOENT;
+ }
+
+ ret = sysfs_create_group(module_kobj, &jump_group);
+ if (ret) {
+ pr_err("jump: Failed to create sysfs\n");
+ return ret;
+ }
+ if (ret) kobject_put(module_kobj);
+
+ return 0;
+}
+
+late_initcall(init_events_group);
- load and run your code with run_file.py script like:
#!/usr/bin/python3
# Load executable file(bin format, use objcopy to convert obj file to bin),
# to physical memory, starting from reboot code buffer address, found in kmsg.
#
# Then timeout for user to switch from usb cable to debug uart cable
#
# After timeout, commands kernel to shutdown, and execute loaded code
#
# Remember if you actually need to switch from usb ssh, to debug uart cable, run this script like
# nohup python run_file.py [file] & tail -f nohup.out
import os
import re
import time
import sys
base_address = os.popen("sudo cat /sys/kernel/jump/show_jump_address").readline().rstrip()
print('Found address for code loading: ' + base_address)
command = 'sudo dd if=' + sys.argv[1] + ' of=/dev/mem bs=16 seek=' + str(int(base_address, 0) >> 4)
os.system(command)
print('Code loaded...')
timeout = 20
for i in range(timeout, 0, -1):
time.sleep(1)
print(str(i) + ' seconds until launch')
print('go')
os.system('sudo bash -c "echo 1 > /sys/kernel/jump/jump"')
- On systems which don't have python(for example TWRP), use sh
#!/bin/sh
BASE_ADDRESS=`cat show_jump_address`
echo 'Found address for code loading: '$BASE_ADDRESS
CMD='sudo dd if='$1' of=/dev/mem bs=16 seek='$(expr $(printf "%d\n" $BASE_ADDRESS) / 16)
CMD
echo 'Code loaded...'
TIMEOUT=20
echo 'Waiting for '$TIMEOUT' seconds timeout'
sleep $TIMEOUT
echo 'go'
echo 1 > /sys/kernel/jump/jump
TODO: add a section to check that we have working UART on USB, and a working payload. We may check if the payload is working with ARM debug coprocessor. See also https://stackoverflow.com/questions/69256603/arm-how-does-linux-decompression-routine-outputs-characters-on-uart |
Running bootloader builds
- Connect to your phone via SSH
- Build the bootloader bin file and upload it onto the phone.
- Run
nohup python3 -u run_file.py [bootloader file] & tail -f nohup.out
. It will give you a 20 second timeout to plug in your debug UART cable, and switch MUIC. - After the timeout is expires, your bin file will be launched, with MMU disabled.
Troubleshooting
No output from your UART
DMA UART mode.
Linux may run UART in DMA mode, instead of FIFO, like a typical bootloader. For example, qcom SoCs GENI core UART works in data mover mode, using DMA, so you need to switch it to FIFO mode. Refer to early_con UART setup in linux and setup UART via devmem accordingly
UART is reset by bootloader.
Refer to early_con UART setup in linux
Phone reboots after executing bootloader
Turn off the watchdog timer. Instructions on how to do so were provided in previous sections.
This category currently contains no pages or media.