最近在给一个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   0x20void myputc(int ch){        while (!(LSR & UART_LSR_THRE)) ;        THR = ch;} | 
在init/main.c的start_kernel()函数开始处添加调试,发现无效,应该是开启mmu了,不能直接使用物理地址。在正常的内核中,相同地方添加,发现内核启动失败了,也证实可能是地址的问题。下面对地址进行分析。
在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:
如上面的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()完成的。
内核地址空间,有一部分作为内核虚拟地址空间,这个空间对应的物理页是动态分配的,而不是线性映射。分配接口是vmalloc。由于是动态分配的,这就导致虚拟地址连续,但物理地址可能不连续。
ioremap()也和vmalloc()一样,也是使用的内核虚拟地址空间。
内核虚拟地址空间范围:[VMALLOC_START, VMALLOC_END)
VMALLOC_START是high_memory + 8M,保留一个8M的hole,用来检测内存越界访问。
见:arch/arm/include/asm/pgtable.h
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);
内核模块有自己的内核地址空间。范围定义在
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。
回到正题
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