Category:Bootloaders/Bootloaders porting using linux

From postmarketOS Wiki


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:

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
Icon 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.