最近在给一个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了,不能直接使用物理地址。在正常的内核中,相同地方添加,发现内核启动失败了,也证实可能是地址的问题。下面对地址进行分析。
在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