linux 启动过程临时页表到底重映射扇区计数 多大了多大内存

2035人阅读

很久没有写博客了,由于之前的写关于OMAP3530文章还没有整理。再加上一直在找工作,找到工作后又投入到另外的平台去工作。始终在忙忙碌碌,但是对于代码确实渐渐疏远。
在做项目的时候要使用DDR3分配内存,不经意间使用要和MMU以及TLB打交道。因此特地写下这篇文章以备后用。(工作就是在和遗忘作斗争)!
Linux在启动之初会建立临时页表,但是在start_kerne函数中setup_arch又会建立正真的页表和页目录。那么两套方案是如何过渡的?假如在MMU开启的时候把之前的临时页表给覆盖了或者修改了,会不会影响后续的启动过程?带着这些问题分析一下。
首先来看一下基于ARM的页表管理和MMU的行为分析:
Arm上的linux(正式)页表采用的是一级粗页表结合二级小页表实现4G空间的访问。如上图说明。
一级表 (1024 Entrys)
二级表 (1024 Entrys)
虚拟地址后12位Offset寻址空间是4096B 4k的空间
Arm上的linux(临时)页表采用的是段式页表,每一个entry可以映射1M的空间,结合后面的20bits位(寻址空间正好是1M)
一级表 (4096 Entrys)
虚拟地址的后20位offset寻址空间是1M
接着来看一下linux如何建立页表的过程。
Head.S中有一段使用汇编编写的初始化代码。Mmu.c中有一段使用c语言写的建立页表的代码。C语言的代码很经典,可能汇编更经典。这里不多分析了。可以百度文章很多分析。
关键问题在于一个变量swap_pgdir
1..macro&&&&pgtbl,&rd&&
3.&&&&&&&ldr&&&\rd,&=(KERNEL_RAM_PADDR&-&0x4000)&&
KERNEL_RAM_PADDR = 0 x XXXXXXXX这是内存的物理地址,那么页表的建立也在这物理地址相关的区域内。
临时页表使用的是段式映射,也称之为平坦映射。那么4G的空间划分为1M为单位的访问单元,需要4096个Entrys。应为Arm采用32位的数据线,因此每一个Entry占用一个32位的区段,也就是4B。
正式页表建立的过程分为二级映射也寻找index的过程。每次把线性地址划分为两段,每一段都作为索引根据TLB BASE的便宜寻找下一级的索引项。最后结合虚拟地址的最后偏移(10
bit)作为依据在4K的空间内寻址。
问题来了,这两种映射会不会应为后一种映射的建立把之前的映射破坏掉,导致linux一个复杂的寻址系统无法正常工作呢?答案肯定不会。
图示比文字描述来的直接,还是直接上两张图说明问题:
由上图可知:临时页表建立的空间和正式页表建立的空间分别部署于不同的空间,因此不会出现覆盖或者修改等现象。同时一二级页表项目录中的内容页值得研究。最后两位同时表现出来的控制逻辑,让MMU翻译地址的过程中有章可循。结合MMU中的AP位规定了访问空间的属性,是否可以访问拒绝访问等。
&&& 最后希望图示可以帮助读者理解映射的意图。文中难免有些地方会引起歧义或者不足之初,希望linux大侠指正点评。
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&谢谢


参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:56966次
排名:千里之外
原创:24篇
评论:14条
(2)(1)(1)(1)(1)(2)(1)(1)(2)(1)(3)(2)(2)(3)(3)(1)(4)经检测你所在的网络可能存在爬虫,因资源限制,我们只能拒绝你的请求。
如果你是推酷的用户,可以以继续访问使用。
如有疑问,可将IP信息发送到
请求解封。博客访问: 87688
博文数量: 40
博客积分: 130
博客等级: 入伍新兵
技术积分: 427
注册时间:
APP发帖 享双倍积分
IT168企业级官微
微信号:IT168qiye
系统架构师大会
微信号:SACC2013
分类: LINUX
linux内核初始化的时候要启动分页,既然要启动分页就要有相应的页表,有页表就要有页目录,很多人都知道系统启动的时候要把物理地址的0-8m映射到虚拟地址的0-8m还要再映射到768m-768m+8m,这到底是为什么呢?
目录的一个目录项映射4m的内存,为了映射8m的内存就要两个目录项,具体就是第一个和第768个目录项映射前8m的物理内存,而第二个和第769个目
录项映射第4到8m的物理内存,在内核启动到startup_32的时候分页还没有开启,但是已经进入保护模式了,也就是说,指令和数据的寻址已经成了从
段选择子里面取索引值然后查gdt而得到的线性地址了,又因为linux采用平坦模式,事实上避开了硬件分段,那么寻址的每一个地址就直接是物理地址了,
但是一旦开启分页,也就是cr0的pg位被置位,那么地址就变为虚拟地址了,就要通过而且必须通过页目录页表映射了,那么考虑下面的代码:
& * Enable paging
&&&&&&&& movl $swapper_pg_dir-__PAGE_OFFSET,%eax& /*设置页目录物理地址*/
&&&&&&&& movl %eax,%cr3&&&&& /* set the page table pointer.. *///加载页目录物理地址到cr3
&&&&&&&& movl %cr0,%eax
&&&&&&&& orl $0x,%eax
&&&&&&&& movl %eax,%cr0&&&&&&&&& /* ..and set paging (PG) bit */开启分页
&&&&&&&& ljmp $__BOOT_CS,$1f&&&& /* Clear prefetch and normalize %eip */
么请问执行到09行的时候用的是什么地址呢?答案是虚拟地址,我们姑且认为08行指令的地址是x,那么09行的显然是x+1,08行的是物理地址,而09
行的成了虚拟地址,这怎么办呢?09行的地址x+1就不能像08行的x那样直接放到地址总线等待片选了,而必须要通过页表查询了,但是指令ljmp
$__BOOT_CS,$1f确实在物理地址x+1处,那么只好将虚拟地址x+1映射到物理地址x+1了,这就是原因,随着这一条指令执行完毕,就真正跳
转到内核地址空间了,__BOOT_CS是个长跳基址,在内核空间。
阅读(748) | 评论(1) | 转发(0) |
相关热门文章
给主人留下些什么吧!~~
这篇文章帮我解决了一个大问题,&开启分页后,&下一条指令执行不了了
请登录后评论。arm-linux内存管理学习笔记(2)-内核临时页表的建立分析
学习了arm内存页表的工作原理,接下来就开始咱们软件工程师的本职工作,对内核相关代码进行分析。内核代码那么复杂,该从哪里下手呢,想来想去。其实不管代码逻辑如何复杂,最终的落脚点都是在对页表项的操作上,那么内核是在什么时机会对页表项进行操作,如何操作?
对于一个页表项,抛开所有的软件复杂逻辑,操作无非就是2种吧。一是填写更新页表项,二是读取获取页表项。
MMU负责根据页表项进行虚实地址转换,因此读取获取页表项的工作是MMU硬件完成,软件是不参与的。内核代码的主体工作是来更新内存页表。页表更新的时机我的理解有4个:
(1)内核启动初期开启MMU阶段临时内存页表的建立
(2)start_kernel中完整的静态内存页表的建立
(3)内核空间中动态映射页表的建立,如vmalloc ioremap dma_alloc_coherent等
(4)用户空间访问虚拟地址时的缺页异常处理函数do_page_fault
由于内核空间的动态映射以及用户空间缺页异常处理中都涉及到了内存分配,放到后面再来分析,我们先来分析前2点。
并且除了第1点启动初期的临时页表是汇编中建立的外,其他3点无论其软件逻辑如何复杂,最终我们会看到都是调用了set_pte_ext来设置内存页表。
今天这篇文章先来分析临时内存页表的建立。
ARM在上电启动时MMU是关闭的,此时CPU访问的地址就是物理地址,并且在kernel之前的bootloader中也不会开启MMU,因此kernel开始运行时MMU是关闭的,这时kernel的运行地址与链接地址不一致。kernel是通过计算链接地址与运行地址的offset来对全局变量进行寻址的。
这个痛苦的过程需要一直持续到MMU开启,当然在开启MMU之前需要将内存页表准备好,这个阶段kernel是建立了一个临时页表,完成了kernel_start到kernel_end的线性映射,这里页表映射采用的是section-mapping,内核代码在静态映射时更喜欢使用section-mapping,但是如果要映射地址不是1MB对齐,则使用page-mapping。这个在后面完整页表建立时还会有体现。
临时页表的建立以及为了完成开启MMU后地址的无缝过渡而建立的平映射,我在之前学习start_kernel之前页表建立的博文中有详细分析,临时页表建立的逻辑在这篇博文中说的很详细了,今天就不说了,今天主要来分析下我对页表建立一些细节的疑问。
(1)临时页表采用section-mapping,那么在第一级页表项中除了高12位物理地址,linux对剩余20位的控制位如何填写
在start_kernel之前页表建立的博文中分析过,create_page_tables在填写页表前,会由proc_info_list结构体中获取__cpu_mm_mmu_flags,然后与物理地址相于,填写页表项。
以公司的armv7处理器为例,proc_info_list结构体的获取与CPU ID相关,在kernel启动的__lookup_processor_type函数中进行获取,根据CPU ID获取相应的proc_info_list结构体,armv7的结构体在./arch/arm/mm/proc-v7.S,如下:
.section &..init&, #alloc, #execinstr
* Standard v7 proc info content
.macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0
ALT_SMP(.long
PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
ALT_UP(.long
PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
cpu_arch_name
cpu_elf_name
HWCAP_S | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
HWCAP_EDSP | HWCAP_TLS | \hwcaps
cpu_v7_name
v7_processor_functions
v7wbi_tlb_fns
v6_user_fns
v7_cache_fns
#ifndef CONFIG_ARM_LPAE
* ARM Ltd. Cortex A5 processor.
__v7_ca5mp_proc_info, #object
__v7_ca5mp_proc_info:
0x410fc050
0xff0ffff0
__v7_proc __v7_ca5mp_setup
__v7_ca5mp_proc_info, . - __v7_ca5mp_proc_info
* ARM Ltd. Cortex A9 processor.
__v7_ca9mp_proc_info, #object
__v7_ca9mp_proc_info:
0x410fc090
0xff0ffff0
__v7_proc __v7_ca9mp_setup
__v7_ca9mp_proc_info, . - __v7_ca9mp_proc_info
/* CONFIG_ARM_LPAE */
* ARM Ltd. Cortex A7 processor.
__v7_ca7mp_proc_info, #object
__v7_ca7mp_proc_info:
0x410fc070
0xff0ffff0
__v7_proc __v7_ca7mp_setup, hwcaps = HWCAP_IDIV
__v7_ca7mp_proc_info, . - __v7_ca7mp_proc_info
* ARM Ltd. Cortex A15 processor.
__v7_ca15mp_proc_info, #object
__v7_ca15mp_proc_info:
0x410fc0f0
0xff0ffff0
__v7_proc __v7_ca15mp_setup, hwcaps = HWCAP_IDIV
__v7_ca15mp_proc_info, . - __v7_ca15mp_proc_info
* Match any ARMv7 processor core.
__v7_proc_info, #object
__v7_proc_info:
0x000f0000
@ Required ID value
0x000f0000
@ Mask for ID
__v7_proc __v7_setup
__v7_proc_info, . - __v7_proc_info
这段汇编初看会比较晦涩,首先是定义了宏定义__v7_proc,之后定义了一系列proc_info_list结构体,其中使用__v7_proc完成了其中部分成员的赋值。而proc_info_list的定义如下:
struct proc_info_list {
unsigned int
unsigned int
unsigned long
__cpu_mm_mmu_ /* used by head.S */
unsigned long
__cpu_io_mmu_ /* used by head.S */
unsigned long
/* used by head.S */
const char
const char
unsigned int
const char
struct processor
struct cpu_tlb_fns
struct cpu_user_fns *
struct cpu_cache_fns
结构体的第三个和第四个成员是与MMU相关的标志位,结合__v7_proc的定义,可以得出,对于armv7,__cpu_mm_mmu_flags __cpu_io_mmu_flags定义如下:
.macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0
ALT_SMP(.long
PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
ALT_UP(.long
PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
ALT_SMP和ALT_UP应用于多核处理器,如果没有定义CONFIG_SMP,则ALT_SMP为空,使用ALT_UP内容,反之使用ALT_SMP内容。这里以单核处理器为例。这样最终可以得出:
__cpu_mm_mmu_flags = PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | PMD_SECT_AF | PMD_FLAGS_UP
__cpu_io_mmu_flags = PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | PMD_SECT_AF
__cpu_mm_mmu_flags用于建立内核代码的线性映射。
__cpu_io_mmu_flags用于早期对于IO空间的映射,如我们需要一个早期的调试串口,需要将串口寄存器空间进行映射。
控制位的宏定义如下,在./arch/arm/include/asm/pgtable-2level-hwdef.h中。
* + Level 1 descriptor (PMD)
#define PMD_TYPE_MASK
(_AT(pmdval_t, 3) && 0)
#define PMD_TYPE_FAULT
(_AT(pmdval_t, 0) && 0)
#define PMD_TYPE_TABLE
(_AT(pmdval_t, 1) && 0)
#define PMD_TYPE_SECT
(_AT(pmdval_t, 2) && 0)
#define PMD_BIT4
(_AT(pmdval_t, 1) && 4)
#define PMD_DOMAIN(x)
(_AT(pmdval_t, (x)) && 5)
#define PMD_PROTECTION
(_AT(pmdval_t, 1) && 9)
#define PMD_SECT_BUFFERABLE (_AT(pmdval_t, 1) && 2)
#define PMD_SECT_CACHEABLE
(_AT(pmdval_t, 1) && 3)
#define PMD_SECT_XN
(_AT(pmdval_t, 1) && 4)
#define PMD_SECT_AP_WRITE
(_AT(pmdval_t, 1) && 10)
#define PMD_SECT_AP_READ
(_AT(pmdval_t, 1) && 11)
#define PMD_SECT_TEX(x)
(_AT(pmdval_t, (x)) && 12)
#define PMD_SECT_APX
(_AT(pmdval_t, 1) && 15)
#define PMD_SECT_S
(_AT(pmdval_t, 1) && 16)
#define PMD_SECT_nG
(_AT(pmdval_t, 1) && 17)
#define PMD_SECT_SUPER
(_AT(pmdval_t, 1) && 18)
#define PMD_SECT_AF
(_AT(pmdval_t, 0))
#define PMD_SECT_UNCACHED
(_AT(pmdval_t, 0))
#define PMD_SECT_BUFFERED
(PMD_SECT_BUFFERABLE)
#define PMD_SECT_WT
(PMD_SECT_CACHEABLE)
#define PMD_SECT_WB
(PMD_SECT_CACHEABLE | PMD_SECT_BUFFERABLE)
#define PMD_SECT_MINICACHE
(PMD_SECT_TEX(1) | PMD_SECT_CACHEABLE)
#define PMD_SECT_WBWA
(PMD_SECT_TEX(1) | PMD_SECT_CACHEABLE | PMD_SECT_BUFFERABLE)
#define PMD_SECT_NONSHARED_DEV
(PMD_SECT_TEX(2))
结合第一篇页表硬件原理中《section-mapping的页表项位定义》一图,可以看出这些宏定义都是根据mmu内存页表的硬件特性进行定义的。展开这些宏定义可以看出:
__cpu_mm_mmu_flags指定了临时页表映射的内核空间是section-mapping方式,可读可写,并且是cached的。
__cpu_io_mmu_flags指定了其所映射的IO区域是section-mapping方式,可读可写,但是是uncached的。
(2)临时页表完成映射的内核空间有多大?
内核完成了对KERNEL_START到KERNEL_END的映射,到底是有多大空间。首先来看这2个宏定义。在arch/arm/kernel/head.S中
#define KERNEL_RAM_VADDR
(PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_START
KERNEL_RAM_VADDR
#define KERNEL_END
PAGE_OFFSET + TEXT_OFFSET一般是0xc0008000,是内核镜像的开始。_end是在arch/arm/kernel/vmlinux.ld.S中定义的内核镜像的结尾。因此KERNEL_START到KERNEL_END就是内核的所有数据。
那么就有问题了,首先内核所有数据就是咱们编译出来的Image吗?
答案是否定的,以我的代码为例,通过如下方法可以验证:
zk@server2:~/workplace/kernel$ size vmlinux
hex filename
b80f3e vmlinux
zk@server2:~/workplace/kernel$ ls -l arch/arm/boot/Image
-rwxr-xr-x 1 zk git 6-04-06 09:44 arch/arm/boot/Image
仔细观察会发现,Image大小基本等于代码段和数据段之和。
这里我们需要知道的是bss段在objcopy时是不会被打包到二进制镜像中的,这是因为bss段中数据全为0,所有的裸序(linux bootloader)都会在启动过程中根据链接脚本中定义的bss段始末地址来对bss段进行清空初始化,它肯定是要被映射的,这也就说明Image大小并不能代表我们临时页表映射的内核空间大小。
临时页表映射的大小要从链接脚本中找,arm的链接脚本是arch/arm/kernel/vmlinux.ld.S。可以看到_end定义在了bss段之后。具体的地址有两种方法可以获取。
一种是arm-readelf -s vmlinux查看各个段的布局。我的vmlinux如下:
zk@server2:~/workplace/kernel$ readelf -S vmlinux
There are 34 section headers, starting at offset 0x31e2f0c:
Section Headers:
ES Flg Lk Inf Al
[ 1] .head.text
[ 2] .text
c200 2ee818 00
[ 3] .rodata
c02f0 0af740 00
[ 4] __bug_table
c03a0 0048fc 00
[ 5] __ksymtab
c03ab03c 3ab03c
[ 6] __ksymtab_gpl
c03b095c 3b095c
[ 7] __ksymtab_strings PROGBITS
c03b3eec 3b3eec 013dd2 00
[ 8] __param
c03c7cc0 3c7cc0
[ 9] __modver
c03c82e0 3c82e0 000d20 00
[10] .init.text
[11] .exit.text
c03e2c30 3e2c30
c03e2d34 3e2d34
[14] .init.tagtable
c03e2d74 3e2d74
[15] .init.pv_table
c03e2db4 3e2db4 00054c 00
[16] .init.data
[17] .data
[18] .notes
c0a5c500 a5c500
c0a5c540 a5c524 12e7b0 00
[20] .ARM.attributes
ARM_ATTRIBUTES
c524 00002b 00
[21] .comment
c54f 008ff3 00
[22] .debug_line
542 1ff6ba 00
[23] .debug_info
bfc 1fa94da 00
[24] .debug_abbrev
c0e0d6 0f6fd5 00
[25] .debug_aranges
d050b0 008ca8 00
[26] .debug_ranges
d0dd58 0b88e0 00
[27] .debug_pubnames
[28] .debug_str
[29] .debug_frame
ed8e88 08f780 00
[30] .debug_loc
[31] .shstrtab
e2d9d 00016f 00
[32] .symtab
e345c 0e7a10 10
[33] .strtab
cae6c 09aad7 00
根据链接脚本定义,_end位于bss段的末尾,因此是0xc0a5c540 + 0x12e7b0 = 0xc0b8acf0
另一种计算方式更加简单,因为_end是链接脚本中定义的符号,因此在编译完成后会在System.map文件中记录他的地址。如下:
c0b8acb4 b cache_cleaner
c0b8ace0 b __key.38242
c0b8ace0 b __key.42691
c0b8ace0 b __key.42767
c0b8ace0 b __key.5625
c0b8ace0 B rpc_debug
c0b8ace4 B nfs_debug
c0b8ace8 B nfsd_debug
c0b8acec B nlm_debug
c0b8acf0 A __bss_stop
c0b8acf0 A _end
2种方式获取的_end地址一致,因此实际临时页表需要映射的空间大小有0xc0b8acf0 - 0xc0008000 = 0xb82cf0,是字节,大约是11.5MB,由于section-mapping是1MB对齐,因此这里内核临时页表完成了12MB空间的映射。
(3)在完整页表建立之前内核的函数栈等需要临时分配的空间临时页表有映射到吗?
要弄明白这个问题,还是要从链接脚本arch/arm/kernel/vmlinux.ld.S中找。在data段中可以看到如下符号定义。
.data : AT(__data_loc) {
_data = .;
/* address in memory */
_sdata = .;
* first, the init task union, aligned
* to an 8192 byte boundary.
INIT_TASK_DATA(THREAD_SIZE)
这里初始化了8KB的空间作为内核栈空间,kernel启动过程中在汇编初始化完成后,会清空bss段,并将该空间顶端地址交给SP栈指针。然后跳转到start_kernel执行。内核的栈指针就在这8KB空间内向下生长。这8KB空间在内核数据空间中,因此在临时页表中已经完成了映射。
也就是说临时页表完成的内核空间映射,已经包括了text段 data段 bss段以及内核栈部分。
这里讨论了elf文件和二进制镜像文件,让我想起了一个有意思的小问题值得记下来。
有个朋友问,如果我把helloworld编译生成的a.out后面追加一些数据,a.out还能正常运行吗?
kernel编译生成的vmlinux是elf文件,elf文件是执行链接文件,其中会记录该程序各个段的信息。我们编写应用程序编译生成的a.out也是elf文件,a.out是可以直接在linux操作系统上运行的,这是因为linux支持对elf文件的解析运行(内核menuconfig可以选择)。
需要搞明白的是,linux系统上执行./a.out运行,其实并不是运行的a.out这个elf文件。
据我的了解,内核对于要运行的elf文件会调用load_elf_binary进行解析,首先是根据elf文件的header信息获取它需要的解释器(如ld),然后加载需要运行的各个段(如data text等)到内存中,把控制权交给解释器,解释器会加载该程序需要动态链接库(静态链接就不运行解释器),最后解释器将控制权交给内存中的程序入口,程序开始运行。
所以说a.out后面追加数据对程序运行没有影响,因为这部分数据没有记录在a.out的段信息中,加载到内存中的数据是a.out的代码段 数据段等,追加的数据根本不会被加载到内存中。
不过对于kernel bootloader,这些都是属于裸编 裸跑程序,虽然他们不依赖与第三方库,不需要解释器,但是并没有第三方程序对其elf进行解析。所以kernel bootloader编译时会生成elf文件,但是最后会objcopy生成纯二进制文件,加载到内存中运行的都是二进制镜像文件。
内核临时页表就分析到这里,接下来开始分析start_kernel之后内核完整页表如何一步步建立起来。
(window.slotbydup=window.slotbydup || []).push({
id: '2467140',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467141',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467142',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467143',
container: s,
size: '1000,90',
display: 'inlay-fix'
(window.slotbydup=window.slotbydup || []).push({
id: '2467148',
container: s,
size: '1000,90',
display: 'inlay-fix'

我要回帖

更多关于 60db的噪音到底有多大 的文章

 

随机推荐