ILD

read/write device register at kernel boot stage
作者:Yuan Jianpeng 邮箱:yuanjp89@163.com
发布时间:2024-10-25 站点:Inside Linux Development

背景

最近在给一个aarch64的soc移植最新的内核,内核串口日志还看不到,需要在启动过程中添加日志,可以通过读写gpio来显示led灯,或者设置uart寄存器输出字符的方式进行调试 。


例如uart输出字符,代码如下,需要通过读写设备寄存器来实现。

1
2
3
4
5
6
7
8
9
#define THR     (*(volatile unsigned int *)0x11002000)
#define LSR     (*(volatile unsigned int *)0x11002014)
#define UART_LSR_THRE   0x20
 
void myputc(int ch)
{
        while (!(LSR & UART_LSR_THRE)) ;
        THR = ch;
}


在init/main.c的start_kernel()函数开始处添加调试,发现无效,应该是开启mmu了,不能直接使用物理地址。在正常的内核中,相同地方添加,发现内核启动失败了,也证实可能是地址的问题。下面对地址进行分析。


Virtual Memory

在MMU开启后,CPU访问的地址就变成了虚拟地址。MMU会把虚拟地址映射到物理地址。

    Virtual Memory is an address mapping

    Maps virtual address space to physical address space


在arm等体系结构上,RAM和device都是通过地址空间访问的。所以MMU不仅对RAM有效,对device寄存器的地址同样有效。


一个arch通常会将虚拟地址分成多个地址段,比如aarch64的memory layout如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
 Start                 End                     Size            Use
 -----------------------------------------------------------------------
 0000000000000000      0000ffffffffffff         256TB          user
 ffff000000000000      ffff7fffffffffff         128TB          kernel logical memory map
[ffff600000000000      ffff7fffffffffff]         32TB          [kasan shadow region]
 ffff800000000000      ffff80007fffffff           2GB          modules
 ffff800080000000      fffffbffefffffff         124TB          vmalloc
 fffffbfff0000000      fffffbfffdffffff         224MB          fixed mappings (top down)
 fffffbfffe000000      fffffbfffe7fffff           8MB          [guard region]
 fffffbfffe800000      fffffbffff7fffff          16MB          PCI I/O space
 fffffbffff800000      fffffbffffffffff           8MB          [guard region]
 fffffc0000000000      fffffdffffffffff           2TB          vmemmap
 fffffe0000000000      ffffffffffffffff           2TB          [guard region]


4.19 arm内核打印的memory layout(修改%p为%px,否则指针打印为ptrval)如下:

1
2
3
4
5
6
7
8
9
10
11
[    0.000000] Virtual kernel memory layout:
[    0.000000]     vector  : 0xffff0000 - 0xffff1000   (   4 kB)
[    0.000000]     fixmap  : 0xffc00000 - 0xfff00000   (3072 kB)
[    0.000000]     vmalloc : 0xda000000 - 0xff800000   ( 600 MB)
[    0.000000]     lowmem  : 0xb0000000 - 0xd9800000   ( 664 MB)
[    0.000000]     pkmap   : 0xafe00000 - 0xb0000000   (   2 MB)
[    0.000000]     modules : 0xaf000000 - 0xafe00000   (  14 MB)
[    0.000000]       .text : 0xb0008000 - 0xb0900000   (9184 kB)
[    0.000000]       .init : 0xb0c00000 - 0xb0d00000   (1024 kB)
[    0.000000]       .data : 0xb0d00000 - 0xb0d640a0   ( 401 kB)
[    0.000000]        .bss : 0xb0d640a0 - 0xb0dac710   ( 290 kB)


它的配置如下:

CONFIG_VMSPLIT_3G_OPT=y

CONFIG_PAGE_OFFSET=0xB0000000


后续内核的一个提交,把arm的memory layout打印去掉了:

commit 1c31d4e96b8c205fe3aa8e73e930a0ccbf4b9a2b
Author: Geert Uytterhoeven <geert@linux-m68k.org>
Date:   Tue Jan 8 14:27:01 2019 +0100

    ARM: 8820/1: mm: Stop printing the virtual memory layout
    
    Since commit ad67b74d2469d9b8 ("printk: hash addresses printed with
    %p"), the virtual memory layout printed during boot up contains "ptrval"
    instead of actual addresses:

Kernel logical address

如上面的aarch64的memory layout,有一个kernel logical memory map,这个地址空间就是普通的内核地址空间

Normal address space of the kernel。


内核代码段、数据段、内核栈、kmalloc分配的地址等等内存,都是在这个地址空间。这个地址空间的物理内存是连续的。为什么呢?因为这个地址段是线性映射。如果内核地址连续,那么对应的物理地址也是连续的。


对于arm32架构,内核逻辑地址映射



在arm32位内核中,CONFIG_PAGE_OFFSET这个内核配置项定义了用户地址空间和内核地址空间的分界。这个配置不是直接设置的,内核提供了几个选项,比如CONFIG_VMSPLIT_3G,就是按3G切割,用户空间3G,那么对应的CONFIG_PAGE_OFFSETS就是:0xC0000000


如果按3G切割,对于32位arm,内核空间只有1G,那么内核逻辑地址空间小于1G(还有内核虚拟空间、fix map等)。如果ram大于1G,那么高于1G的内存,内核逻辑地址是无法使用的,这个内存是高位内存(high memory)。


arm 32位内核,内核逻辑地址范围:[PAGE_OFFSET, high_memory)


high_memory, vmalloc_limit等参数的计算,是在arch/arm/mm/mmu.c: adjust_lowmem_bounds()完成的。

Kernel virtual address

内核地址空间,有一部分作为内核虚拟地址空间,这个空间对应的物理页是动态分配的,而不是线性映射。分配接口是vmalloc。由于是动态分配的,这就导致虚拟地址连续,但物理地址可能不连续。


ioremap()也和vmalloc()一样,也是使用的内核虚拟地址空间。


内核虚拟地址空间范围:[VMALLOC_START, VMALLOC_END)

VMALLOC_START是high_memory + 8M,保留一个8M的hole,用来检测内存越界访问。

见:arch/arm/include/asm/pgtable.h


Kernel fixed map

compile-time virtual memory allocation

顾名思义,是内核启动后,创建的固定映射。

头文件:arch/arm/include/asm/fixmap.h


fixed map的范围,用下面两个宏表示:

#define FIXADDR_START           0xffc00000UL
#define FIXADDR_END             0xfff00000UL

#define FIXADDR_TOP             (FIXADDR_END - PAGE_SIZE)


有各种固定类型的fixed map,用枚举fixed_addresses表示,

enum fixed_addresses {
        FIX_EARLYCON_MEM_BASE,


include/asm-generic/fixmap.h

定义了相关接口:

unsigned long fix_to_virt(const unsigned int idx)

unsigned long virt_to_fix(const unsigned long vaddr)

unsigned long virt_to_fix(const unsigned long vaddr)


第一个类型,就是early console用来映射它的寄存器地址。

drivers/tty/serial/earlycon.c

set_fixmap_io(FIX_EARLYCON_MEM_BASE, paddr & PAGE_MASK);

base = (void __iomem *)__fix_to_virt(FIX_EARLYCON_MEM_BASE);


Kernel modules address

内核模块有自己的内核地址空间。范围定义在

arch/arm/include/asm/memory.h

[MODULES_VADDR, MODULES_END)


模块地址空间,从PAGE_OFFSET-16M开始,到PAGE_OFFSET-PMD_SIZE或PAGE_OFFSET结束,大概是16M。


那个头文件还定义了:TASK_SIZE,这个就是用户进程空间的大小,[0, TASK_SIZE),

TASK_SIZE也是PAGE_OFFSET-16M。


read/write device at kernel boot stage

回到正题


1 显然我们无法通过下面的接口,计算一个虚拟地址来访问设备寄存器:

arch/arm64/include/asm/memory.h

__va()/phys_to_virt()

因为这个接口是针对内核逻辑地址的,上面两个接口不需要查询page table,直接是线性映射计算的。


linear map计算方法:

#define __lm_to_phys(addr)      (((addr) - PAGE_OFFSET) + PHYS_OFFSET)


#define PHYS_OFFSET             ({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })

s64 memstart_addr __ro_after_init = -1;

memstart_addr是内存的起始物理地址,它不是一个配置项,而是在内核启动过程中初始化的。


2 也不能使用ioremap接口

这个接口依赖于slab,而此时slab还未初始化好。


include/asm-generic/io.h

static inline void __iomem *ioremap(phys_addr_t addr, size_t size)


arch/arm64/mm/ioremap.c

mm/ioremap.c

void __iomem *generic_ioremap_prot(phys_addr_t phys_addr, size_t size,
                                   pgprot_t prot)
{
        unsigned long offset, vaddr;
        phys_addr_t last_addr;
        struct vm_struct *area;

        /* An early platform driver might end up here */
        if (WARN_ON_ONCE(!slab_is_available()))
                return NULL;


3 在看fixedmap的时候,发现有预留给ioremap,接口是:

include/asm-generic/early_ioremap.h

mm/early_ioremap.c


extern void __iomem *early_ioremap(resource_size_t phys_addr,
                                   unsigned long size);


这个接口需要初始化后使用

arch/arm64/mm/ioremap.c

void early_ioremap_init(void);


init/main.c

start_kernel() ->

        arch/arm64/kernel/setup.c

        setup_arch()

            -> early_fixmap_init()

            ->early_ioremap_init();


测试发现,要在setup_arch()之后,调用eary_ioremap(),之前调用返回0。


代码如下:

diff --git a/init/main.c b/init/main.c
index b25c779..53c82dc 100644
--- a/init/main.c
+++ b/init/main.c
@@ -873,6 +873,21 @@ static void __init print_unknown_bootoptions(void)
        memblock_free(unknown_options, len);
 }
 
+static unsigned long my_uart_base;
+
+#define THR     (*(volatile unsigned int *)my_uart_base)
+#define LSR     (*(volatile unsigned int *)(my_uart_base + 0x14))
+#define UART_LSR_THRE   0x20
+
+void myputc(int ch) {
+        while (!(LSR & UART_LSR_THRE)) ;
+        THR = ch;
+}
+
+void myputs(const char *str) {
+       while (*str) myputc(*str++);
+}
+
 asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
 void start_kernel(void)
 {
@@ -898,6 +913,12 @@ void start_kernel(void)
        pr_notice("%s", linux_banner);
        early_security_init();
        setup_arch(&command_line);
+
+       my_uart_base = (unsigned long)early_ioremap(0x11002000, PAGE_SIZE);
+       pr_notice("=== uart 0x%lx", my_uart_base);
+       myputs("123\n");
+       early_iounmap((void *)my_uart_base, PAGE_SIZE);
+
        setup_boot_config();
        setup_command_line(command_line);
        setup_nr_cpu_ids();


启动后,输出了123

123
   [    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[    0.000000] Linux version 6.6.31+ (yuanjp@fedora) (aarch64-linux-gnu-gcc


参考

[1] Memory Layout on arm/AArch64 Linux

https://www.kernel.org/doc/html/latest/arch/arm64/memory.html

https://www.kernel.org/doc/html/latest/arch/arm/memory.html


[2] Alan Ott. Virtual Memory and Linux. Embedded Linux Conference. 2016.4

https://events.static.linuxfound.org/sites/events/files/slides/elc_2016_mem.pdf


 

参考:

https://www.kernel.org/doc/html/v6.11/driver-api/io-mapping.html

https://www.kernel.org/doc/html/latest/driver-api/device-io.html

https://linux-kernel-labs.github.io/refs/heads/master/lectures/address-space.html


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.