Bootloaders porting using linux

From postmarketOS
Jump to navigation Jump to search


You must have an uart debug cable, if you want to port bootloader on your phone. However, some bootloaders and/or MUIC chips does 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 debug uart pins, exposed on the board.
  • Hack linux to force 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

Hack MUIC driver to expose uart on request

We can use sysfs attribute to tell muic driver, we need to switch debug uart to usb. For example, 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 new sysfs attribute dev_attr_muic_switch_uart.attr, so that MUIC driver will attach uart on '1' write to that attribute, and detach it on '0' write.

Setup pmos to switch muic on key combination

Use triggerhappy

Build a cable

  • determine, on what pins MUIC multiplexes uart, i.e. find TX RX pins on usb.

The process is similar to finding TX RX pins on the PCB For type-c devices it will likely be either D+D- or SBU pins.

  • assemble cable
  • ensure you get an output from your cable, when writing to corresponding tty on the phone

Hack kernel to execute your bootloader

  • enable CONFIG_DEVMEM
  • disable watchdog timer (it forces reboot, if kernel not responds)

Simplest way is to delete device tree node, for qcom device usually compatible = "qcom,msm-watchdog";

  • apply a patch to launch 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 with no python, like 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

Run bootloader builds

  • Connect to phone via ssh
  • Build bootloader bin file, upload it on phone.
  • Run nohup python3 -u run_file.py [bootloader file] & tail -f nohup.out. It will give you 20 secs timeout to plugin debug uart cable, and switch muic.
  • After timeout is expired, 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 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, 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 watchdog