外设功能函数/跟踪函数与机制分析

项目工程

Posted by Bruce Lee on 2024-09-01

关于我

欢迎来到我的博客!这里汇集了我对编程和技术的洞见和总结。本站内容分为几个主要类别,涵盖从具体技术实现到编程理念的广泛话题。

主要内容分类

  • 项目工程:深入探讨技术的实现细节和理解。
  • C/C++:围绕C/C++语言的技术点和编程技巧进行详细总结。
  • 程序员哲学:分享程序员在职业生涯中应该具备的哲学理念和思考方式。

想要了解更多具体内容,您可以访问文章分类页面。

联系我

如果您有任何问题或想要交流,欢迎通过关于页面与我联系。

感谢您的阅读和支持,希望我的博客能为您的技术旅程带来帮助!


时钟

关于实现__am_timer_uptime函数的时候,并没有机会去获得系统启动时间.

应该将__am_timer_init函数实现,并且声明一个全局的AM_TIMER_UPTIME变量用于存储启动时间.

RTC_ADDR地址处的8字节,是关于时间的信息.依次读取出来.

但是关于i8253计时器的模拟代码中,

关于offset偏移,应该要加上为0的时候,应该也要将端口刷新时间信息.

我们到读取时间信息的时候,是读RTC_ADDR地址的内容,这里的内容是rtc_io_handler函数在做维护.

这个函数被add_mmio_map调用.

其实这里的一系列的函数调用串,有一条是回到的paddr.c文件.

在paddr_write函数中,每一次的访寸,都会调用mmio_write函数

mmio_write函数应该是负责刷新外设接口信息的.

在目标缓存地址地方,都会被刷新信息.

模拟器的时间从根本上来说是调用gettime函数,进而调用get_time_internal函数,然后通过调用gettimeofday函数来获取.

然后模拟了时钟.

在monitor文件中,初始化了device,其中就有init_timer函数,调用了add_mmio_map函数进行获取第一次的时间信息.

在execute中,条件编译了device_update函数,用于每次的更新时钟端口.

在device.c文件中定义device_update函数,

错误缘起

在printf实现的附近,编译到native中,出现了segment fault.

经过gdb的观察,是内存的访问除了错误,重点在printf函数.

可能的原因是klib的实现,并不是严格的gnu标准.

此时,便无法通过运行在native中,来测试klib的正确性.

但是一直一来,都是可以运行在nemu上的.

在运行demo中的程序中,出现了错误,但是代码雨是正确的.

看了源码和开trace,应该是memmove等一系列函数我没有实现.

实现之后,还没有编写klib-test进行测试.

直接开始运行demo

还是错误.

重新理清一下逻辑,然后运行最后一个bf程序,打开ftrace.

观察了调用栈,然后再最后一句,关于malloc的调用上,发现了可疑的问题.

回看malloc代码.

回看bench的实现,发现了遗漏关于hbrk的初始化.

然后补上这个初始化.

这里及时补充的初始化是直接在malloc函数内部进行,过段时间,编写专用的init函数,放置在集成的初始化函数堆里.

然后现在demo全部运行正确.

有趣的io_read宏

在bench.c文件中,uptime函数是如何获取时间信息的.

其实是返回了调用io_read宏的结果.

io_read宏的使用方式是:

1
io_read(AM_TIMER_UPTIME).us

这种用法很少,感觉很有趣.其实这里只需要分析一下io_read的返回值是什么,就知道为何要引用us了.

从这个结构可以猜出,返回的是一个AM_TIMER_UPTIME_T类型变量,引用了内部的us变量.

这中间是怎么起作用的?

io_read的定义中,传入的参数会被 ## 粘结符连接上_T,然后定义了__io_param这个临时变量.

所以传入的AM_TIMER_UPTIME会被粘结成AM_TIMER_UPTIME_T,然后定义一个局部变量.

然后调用了ioe_read函数.

其实传入的AM_TIMER_UPTIME也是一个宏,代表数字4.

在调用ioe_read宏时,是直接返回了lut数组的AM_TIMER_UPTIME项,其实这个返回就是函数地址,然后传入buf,就是局部变量,那么返回了这个局部变量的值,这个值就是时间.

返回到io_read宏中,最后一句就是使用局部变量,返回的就是这个变量值.

putch函数实现

putch函数是调用了outb函数

outb是直接访问了addr地址,将data写入.

这里在putch函数中调用的outb,传入的参数是串口的地址:SERIAL_PORT

关于native和klib的隔离

之前分析的关于在native中运行一直出现段错误,然而在nemu中运行良好的问题.

主要就是自己实现的库和glibc中的标准库不是完美符合的.

在stdlib等几个文件头部,可以重新实现一下条件编译的__ISA_NATIVE__宏.

就可以在定义了__NATIVE_USE_KLIB宏定义的情况下,编译到native中依然是链接glibc中的库.

dtrace实现原理

如果每一次访问device都记录一遍,那么将是一个无底洞.

所以,在连续的对一个device访问时,只记录一次该device,如果更换访问目标,则记录另一个.

并且dtrace记录到Log中包装的nemu-log中,所以dtrace依赖于itrace.

dtrace没有什么大的负担,不如就放入itrace一块,默认开启,并且不用定义DTRACE宏.

每一次的访问device,都是调用了mmio_read/write,这个函数单一调用map_read/write,在map_read/write中进行专门的处理.

所以dtrace代码应该在map_read/write中实现.

问题就是,记录到nemu-log中的访问设备信息,会被指令信息淹没.

所以,在开启dtrace的时候,最好是关闭掉itrace

为何要记录到nemu-log中?

因为要访问设备,大部分是要有输入输出,音频等,如果将访问记录也输出到屏幕中,会扰乱程序的展示效果.

临时,添加了DTRACE,依赖于TRACE

由于要记录device的访问记录,同时也不能输出,所以定义了一个新的宏:log_only,复用了log_write宏.

关于键盘实现

既然有了表示按下/抬起的keydown变量,那么另一个变量理应存储重要的键码,按下键码,而不是释放键码.

所以应该与0x00ffand一下.

多个键同时按下

在按下控制键的时候,保持状态,会输出关于这个键的键码和状态,然后按下其他键的时候,就知道功能键是在控制键为down的状态下按下的.

VGA

关于AM_GPU_CONFIG控制器信息

屏幕的大小信息的确定来自CONFIG_VGA_SIXE_800x600宏的定义.

如果定义了该宏,那么SCREEN_W/H就会是800或400/600或300.

别的地方获取屏幕大小信息,源头是调用了screen_width/height函数,其中有MUXDEF宏的选择性决定屏幕大小.一般而言,我们是定义了CONFIG_TARGET_AM宏,所以返回值一般就是我们的AM_GPU_CONFIG抽象寄存器中的内容.

综上,屏幕大小信息的来源是CONFIG寄存器.所以该寄存器的width和height的决定不可能是是调用别的地方的函数或者从内存中读取出来.

总内存大小

还有另一种信息来源,就是在nemu.h中,定义了RANGE宏,其中关于FB_ADDR的传入参数,有一些透露出,FB_ADDR内存的大小.

关于AM_GPU_FBDRAW帧缓存控制器

坐标,小矩形图像.

绘制算法,就是要按照小矩形图像来换行,早先编写代码,是直接线性填充,出现了色块变线条的bug.其实是填充算法的错误.

需要首先定位到FB_ADDR地址,然后定位到坐标地址.然后嵌套for循环,来填充颜色数据.

在am中的访存

在am中,有多处是直接访问地址数值,例如上述的访问FB_ADDR,由于am也是跑在nemu中,而nemu是全系统模拟器,所以实际上即使代码是访问内存,其实也是被nemu给映射了.

映射原理

屏幕大小寄存器和同步寄存器

硬件的实现,是已经vgactl_port_base申请了内存,向8对齐.申请了8字节.

其中首4字节的高位存储着宽,低位存储着长.

只需要读取vgactl_port_base的高32位,就可以读取出宽和长.

这里的寄存器的写入信息的来源,是前面的分析中体现出的源头.

同步寄存器,连着一块被申请了,因为在gpu.c中定义的SYNC_ADDR宏是定义在VGACTL_ADDR+4的基础上的.

裸着的抽象寄存器

并没有显而易见的定义这两个寄存器以及软件.

屏幕大小寄存器将信息写入了目标地址,但是在引用的时候,我嫌麻烦,并没有直接去访问这里的地址,而是一般直接从__am_gpu_config中读取.

同步寄存器.在一些填充完毕的色块的时候,向SYNC_ADDR地址写入1,这就是向同步寄存器写入1了,我在vga_update_screen函数中,并没有直接访问一个专门定义的同步寄存器的函数或者宏,而是直接判断vgactl_port_base[1]的内存.

所以直接裸着.

全源码剖析

首先,需要了解的就是关于在am中对于地址的直接访问,nemu是如何模拟这一行为的.

关于游戏,或者说是对于一些IO接口的访问以及对于IO的实现,都是建立在对内存的控制上面,如果对于内存控制了解的足够深入,那么再去了解那些游戏运行机制,那么就更容易了.

引起思考就是在nemu的代码中,对于所有的硬件的初始化以及内存布局,都是严格的按照函数调用,层层解析的.

但是在am中对于某一个硬件的访问,访问设备寄存器是直接访问地址,这一不同现象.

在具体的阐释这里的原理是什么之前,我需要列出现象,代码逻辑.

nemu中的设备初始化以及内存布局.

init_device函数将进行一系列的设备初始化,并且按照了一定的顺序,首先就是这里的ioe_init函数.该函数在am中,这个行为属于硬件内部代码调用了驱动代码.

ioe_init函数调用了__am_gpu_init,__am_timer_init,__am_audio_init函数.在之前两个函数的实现当中,是没有内容的,更多的是一种测试代码.并没有内存的相关代码.这些也都是驱动代码

然后开始init_map函数的调用.该函数是硬件代码,

讲这里,就需要将整个设备的内存分配原理.

首先是使用malloc函数,给IO空间分配了一个大地址.这里的属于用驱动代码

IO地址起始

使用了malloc,传入了IO_SPACE_MAX宏,malloc是根据hbrk指针来分配的.

而hbrk是维护整个堆的指针,一开始是根据heap.start值获得的.

heap是在是由_heap_start来获得的.而_heap_start变量是am的linker.ld链接脚本中定义的符号,用于0x1000对齐,也就是4KB内存对齐.

所以我们的堆一开始就是4KB对齐的.

这里随便另外讲一下_pmem_start也是类似的.

目前,依然没有讲关于这个_heap_start的值是多少,对应的内存地址是多少,也就是说,目前来说,这里分配的总IO地址是不知道的.

上述都是在"驱动"中的,然后,io_space就指向了IO总地址,p_space也指向了这里,但是p_space还有维护这个IO内部的内存地址

map的映射初始化完成了.然后开始初始化设备.第一个设备就是串口.

调用了init_serial函数,然后将调用new_space函数来申请了一个端口地址.

这里的new_space函数:
通过p_space来维护IO空间,先是处理传入的size,内部的处理就是向下4KB大小对齐,也就是说,返回的空间最小也是4KB.(一个页大小)

然后p_space指向下一个地址(但是还是在IO空间内).

然后返回申请的首地址.

然后根据条件编译,这里讲地址映射,那么就是调用了add_mmio_map函数.

增加了一个设备,并且记录这个设备的地址映射信息.

同时,这个函数还需要传入设备名称name,设备地址addr,申请的端口地址space,space的大小len,以及回调函数callback.

这里的设备地址addr,就是理论设备地址.这里的space,就是实际的申请的地址,这个地址属于能申请到哪就到哪了.然后就是回调函数.这里的回调函数,就是更新端口信息(可以说是更新设备状态,刷新设备功能.)

这里的serial的回调就是查看写信息,然后直接嗲用serial_putc函数(内部就是调用了标准库的putch/putc函数,物理机),将信息输出.

一般回调函数,就是处理信息,然后处理申请的端口地址,将这里的数据处理掉.

add_mmio_map函数.根据传入的信息.处理space空间.

这些都是硬件实现.

该函数调用in_pmem函数检查,从这里可以看出,space的地址是大于0x80000000,所以在链接的时候,这里就被规定了一个高地址.

然后进行maps检查,这里nr_map记录实际的设备数量,NR_MAP记录最大设备数量.

这里防止和其他设备内存冲突,进行的检查.

然后在maps数据中注册设备,记录所有的信息,然后Log到记录中.

这里的addr数据,被记录到了low变量还总,len记录到了high变量中.其他的对应就可以了.

然后nr_map加1.

.

再次分析一下关于init_timer的分析.

同样,通过new_space申请空间,下一个页面大小,作为端口地址.

然后调用了add_mmio_map函数进行注册.

这里的回调函数就是对申请的空间进行操作.

不对理想设备地址进行操作.

进行了一系列的初始化.

然后开始了

monitor.c初始化完所有之后,一系列简单的调用之后,来到了cpuexec这里

每执行一条指令,就会调用device_update哈数.

这里的update函数,根据get_time函数(实际的标准时间),来调用vga_update_screen函数,进行屏幕的输出.这里就是关于vga的硬件实现以及其申请的空间信息的处理.

nemu的硬件实现就到这里就结束了.

然后来看一个软件方面,"驱动"是怎么利用硬件的.

从一个客户程序为例,rtc测试程序,需要调用io_read宏,传入的是AM_TIMER_UPTIME,然后引用了其us内部变量.

这里就从io_read宏开始是说起.

io_read宏之前分析过,最里面就是调用了lut函数指针数组.

这里的lut函数指针数组都是指向的函数,这里的函数(文档中的抽象寄存器),就是用于读取设备信息的.

最重要的来了.这里在am中是如何获取设备信息的.

这里以timer为例(实际上这里的写法是学习源框架代码写法的,有的地方就是直接访问内存,比如outb宏)

直接访问了RTC_ADDR指向的地址,这里的地址其实就是理想的设备地址,这里是直接访问了.

实际硬件是通过new_space函数申请的,由maps数组中的对应设备号中的space来表示的.

但是这里是直接访问了RTC_ADDR空间.

其实我们可以直接调用mmio_read函数,或者将地址传入paddr_read函数,都会调用到mmio_read函数,这里传入的地址,就是理想设备地址,这也是我们想要模拟的设备的好的处理方式.

我们传入理想地址,会有fetch_mmio_map函数根据理想地址,使用for循环索引maps中的单元,来获取对应的设备信息,以及申请的实际地址.

然后调用map_read/write函数

在这里,就是检查边界,然后根据addr(理想地址),来看看想要获取的设备的偏移地址.

然后调用host_read函数直接读数据.

这里的直接读,就是传入了map->space变量+offset,以及长度.

看来到了最后确实是根据实际的申请空间来读取数据的.

实际的host_read内部,其实是对应上了一开始申请的数组pmem.这是整个程序内存的源头,就是一个向物理机申请的数据.

从这里再往回看,那么_heap_start也是一个在数组中的一个地址值,是不为程序员所知道的.

am中的输出,调用了outb之类的,实际上也是直接向SERIAL_PORT地址写入数据.

这些设备的理想地址,都是定义在nemu.h中,之前在硬件中使用的理想地址是定义在nemu中的autoconf.h文件中.

在nemu.h文件中,定义了这些理想设备地址,并且给了一个_pmem_start变量.

还有一些关于PMEM*的宏,都是依赖与_pmem_start.

_pmem_start来源与linker.ld文件中,一个符号,这个符号在nemu.mk中被赋为了0x80000000.

不管是什么客户程序,在编译的时候,也是和运行时环境一块,和那些编写的库文件,库代码合在了一块,一起执行.

也就是说到了最后,运行时环境就是客户程序,客户程序也是运行环境.

要理解客户程序的运行,可以和理解运行时环境的运行放在一起.

这里在运行时环境的搭建过程中,有些访问都是很合乎情理,不管是内存访问还是逻辑计算(其实主要就是内存访问),都是可以通过层层的函数逻辑来演算.说到底,不如将nemu看成源代码,将输入看成一个包好的程序,这个程序就是运行时环境+客户程序.是一堆二进制码,nemu挨个去读二进制码,然后作出对应的动作,这里的逻辑都是通的.

甚至是写入的数据,然后对应的设备函数,也是被调用.

那么就通了,am中对于内存的直接访问,会被nemu捕获,对应到了其数组中.这里捕获的过程,主要就是编译的帮助,我们在链接的时候,所做的一些动作,可以更好的贴合程序的运行.

这样,就更体现出了nemu作为一个全系统的模拟器.所有的内存操作,到头都是对pmem数组的内部操作,包括nemu自己.而且,这里的mmio对与设备很好的访问函数,对于am是全部封装的.

am只能通过抽象寄存器,访问抽象地址.

声卡与VGA

硬件

使用enum来定义了几个状态寄存器,包括了freq,channels,samples,sbuf_size,init,count.硬件层面来使用audio_base来索引.

流缓存区是使用sbuf来申请了一个0x10000大小的空间.

关于handler函数,注册状态寄存器空间的时候,每一次的状态寄存器的访问,都会回调handler函数.

在handler里面实现了关于SDL库的初始化和OpenAudio函数,PauseAudio函数的调用.

这里每一次只能调用一次,所以加了一个static变量,用于实现只调用这个函数一次

关于回调函数的实现,用于将每一次SDL的申请大小len长度的userdata放入stream流中.

这里为了迎合测试函数,先是测试函数逻辑,便将每次传入定为4096.

软件

宏定义了这些状态寄存器的理想地址.

init函数被取消了.为何?

init_device是用来初始化设备的,第一个条就是用来相对于CONFIG_TARGET_AM宏的ioe_init函数的条件编译.

在ioe_init函数中调用了__am_audio_init函数.

按理说在audio_init函数中访问状态寄存器赋予初始值,是需要先在map地址映射中注册信息的.

但是在此之前是没有注册地址信息的.所以我将其取消了

之前在取出audio的设备进行调试的时候,就因为添加了audio_init的实现,然后出现map中的check_bound错误.

config函数用来返回状态寄存器大小.ctrl用来写回到状态寄存器中的freq,channelds以及samplese.

这些没什么好说的.

主要是play函数,现在有多个版本的实现,最简单就是迎合测试函数的输入.这里先是一个while循环用于等待COUNT寄存器为0.

然后使用for循环将4096个字节传入sbuf流中.

在硬件那边,是用来等待COUNT寄存器不为0.

问题

实现上来说都是正确的,播放时候也是正常,就是速度有点块.在第一天调试的时候,速度调成正常的了,后来就没调回来了:原因将下.

首先在实现声卡的时候,遇到了操作系统返回的错误:

1
*** buffer overflow detected*** terminate

这是操作系统返回的错误,经过很久的调试,找到了原来是string中的strcpy函数实现有问题,这里的问题环境是在试下dtrace中出现的.

取消掉dtrace代码,一切恢复正常.

目前主要就是存在malloc的错误,比较深层次的错误.大概率是内存访问溢出.

错误复现1:

运行demo中的1,2等程序,将memset函数加上一个assert断言.

1
2
//uint32_t s_len = strlen(s);
//assert(n <= s_len + 1);

即可出现bad错误,这个断言用于确定,置位的个数一定小于字符串的大小个数.

错误复现2:vga和audio设备都打开,然后运行vga程序正常.运行audio的test也正常.

草,怎么又正常了.

都打开之后,都能运行.行吧,反正之前是会出现这个错误2:

1
malloc(): corrupted top size

vga的多个线程的反复摧毁和建立3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src/device/io/mmio.c:51 add_mmio_map] Add mmio map 'vmem' at [0xa1000000, 0xa10752ff]
[New Thread 0x7fffeacf0640 (LWP 48391)]
[New Thread 0x7fffea4ef640 (LWP 48392)]
[New Thread 0x7fffe9cee640 (LWP 48393)]
[New Thread 0x7fffe94ed640 (LWP 48394)]
[New Thread 0x7fffe8cdc640 (LWP 48395)]
[New Thread 0x7fffd3fff640 (LWP 48396)]
[Thread 0x7fffd3fff640 (LWP 48396) exited]
[Thread 0x7fffe8cdc640 (LWP 48395) exited]
[New Thread 0x7fffe8cdc640 (LWP 48397)]
[New Thread 0x7fffd3fff640 (LWP 48398)]
[Thread 0x7fffd3fff640 (LWP 48398) exited]
[Thread 0x7fffe8cdc640 (LWP 48397) exited]
[New Thread 0x7fffe8cdc640 (LWP 48399)]
[New Thread 0x7fffd3fff640 (LWP 48400)]
[Thread 0x7fffd3fff640 (LWP 48400) exited]
[Thread 0x7fffe8cdc640 (LWP 48399) exited]
[New Thread 0x7fffe8cdc640 (LWP 48401)]
[New Thread 0x7fffd3fff640 (LWP 48402)]

使用gdb的tui调试出,在SDL_Init函数中,出现的这么多线程的反复创建和调用.
XCB和Xlib库的线程问题4:

1
2
3
4
5
[xcb] Unknown sequence number while appending request
[xcb] You called XInitThreads, this is not your fault
[xcb] Aborting, sorry about that.
riscv32-nemu-interpreter: ../../src/xcb_io.c:157: append_pending_request: Assertion !xcb_xlib_unknown_seq_number' failed.
make[1]: *** [/home/bruce/project-a/git-project/ysyx-workbench/nemu/scripts/native.mk:49: run] Aborted (core dumped)

现在知道了.

在make menuconfig中打开内存的监视器,就会出现问题2.

更好的实现

源码被注释了,就在现在可用代码的下面,已经不想说啥了,检查了每一个循环和调用环境,但是运行的时候还是会出现访问0xa1210000地址.

关键是test中的整个大小也没那么大呀.

nemu上运行nemu

现在的问题就是,取消掉ftrace功能后,必须将am中的makefile中的-lelf编译选项取消掉.因为无法链接.

SDL中的stream问题

传入2048的samples以及以及AUDIO_S16SYS的format,传回来的stream尽然是2048大小的.

这是开了sanitizer后的检测到的.后来修改了传入大小,声音也确实更和谐了


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !