Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
代码加载
进程
K230
程序的起点(rt-smart & k230)
发布于 2024-03-15 20:55:32 浏览:382
订阅该版
[tocm] ## 最简单的程序 相信很多学习C语言的同学都曾写过一个hello world的程序,从控制台or屏幕打印出的字符开始,步入了码农的世界,那么程序本身是怎么被启动的呢?字符又是如何打印出来的呢?接下来我们通过rt-smart的elf加载流程来回答这个问题。代码参考嘉楠科技的k230 sdk中的[rt-smart源码](https://github.com/kendryte/k230_sdk/tree/main/src/big/rt-smart)。 ```c #include
int main(void) { printf("hello world\n"); } ``` ## 程序的编译与链接 一段代码需要进行编译和链接才能被执行,程序在链接过程中会将自己的起始地址固定到某个内存位置。这个固定地址的定义,可以在rt-smart提供的链接脚本中找到。[link.lds](https://github.com/kendryte/k230_sdk/blob/main/src/big/rt-smart/userapps/linker_scripts/riscv64/link.lds) ```bash /* * Copyright (c) 2006-2020, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2020/12/12 bernard The first version */ OUTPUT_ARCH( "riscv" ) ENTRY(_start) SECTIONS { /* * 64bit userspace: 0x200000000 (default) * 32bit userspace: 0xD0000000 */ . = 0x200000000; .text : { __text_start__ = .; *(.start); *(.text*) /*省略*/ } _end = .; } ``` 可以看到链接脚本将程序的起始虚拟地址规定为了0x200000000。为了验证这个猜想,我们将编译好的程序使用objdump反汇编查看。 ```bash riscv64-unknown-linux-musl-objdump hello.elf -D hello.elf: file format elf64-littleriscv Disassembly of section .text: 0000000200000000 <_start>: 200000000: 000001b7 lui gp,0x0 200000004: 00018193 mv gp,gp 200000008: 8132 mv sp,a2 ``` 这样可以确认,当前elf文件第一个执行的代码段的虚拟地址为0x200000000。 ## 程序的加载 在rt-smart的msh命令行中输入hello.elf即可执行该程序并获得打印结果。 ```bash msh /sharefs>hello.elf hello world ``` 接下来我们通过对OS源码的分析来梳理整个程序的运行流程。系统启动后rt-smart内核会创建一个finsh的线程void finsh_thread_entry(void *parameter)用来捕获控制台的输入,当检测到回车或换行字符后,finsh会将从控制台得到的字符串信息作为参数传递给msh_exec来创建一个rt-smart上的进程。 ```c /* handle end of line, break */ if (ch == '\r' || ch == '\n') { #ifdef FINSH_USING_HISTORY shell_push_history(shell); #endif if (shell->echo_mode) rt_kprintf("\n"); msh_exec(shell->line, shell->line_position); rt_kprintf(FINSH_PROMPT); memset(shell->line, 0, sizeof(shell->line)); shell->line_curpos = shell->line_position = 0; continue; } ``` 之后执行msh_exec->_msh_exec_lwp->exec->lwp_execve来真正开始创建进程。以下是该函数的内容信息,为了方便阅读已删除部分代码。 ```c pid_t lwp_execve(char *filename, int debug, int argc, char **argv, char **envp) { int result; rt_base_t level; struct rt_lwp *lwp; char *thread_name; char *argv_last = argv[argc - 1]; int bg = 0; struct process_aux *aux; int tid = 0; int ret; if (filename == RT_NULL) { return -RT_ERROR; } lwp = lwp_new(); if (lwp == RT_NULL) { dbg_log(DBG_ERROR, "lwp struct out of memory!\n"); return -RT_ENOMEM; } LOG_D("lwp malloc : %p, size: %d!", lwp, sizeof(struct rt_lwp)); if ((tid = lwp_tid_get()) == 0) { lwp_ref_dec(lwp); return -ENOMEM; } #ifdef RT_USING_USERSPACE if (lwp_user_space_init(lwp) != 0) { lwp_tid_put(tid); lwp_ref_dec(lwp); return -ENOMEM; } #endif result = lwp_load(filename, lwp, RT_NULL, 0, aux); #ifdef ARCH_MM_MMU if (result == 1) { /* dynmaic */ lwp_unmap_user(lwp, (void *)(USER_VADDR_TOP - ARCH_PAGE_SIZE)); result = load_ldso(lwp, filename, argv, envp); } #endif /* ARCH_MM_MMU */ if (result == RT_EOK) { rt_thread_t thread = RT_NULL; rt_uint32_t priority = 25, tick = 200; lwp_copy_stdio_fdt(lwp); /* obtain the base name */ thread_name = strrchr(filename, '/'); thread_name = thread_name ? thread_name + 1 : filename; thread = rt_thread_create(thread_name, _lwp_thread_entry, RT_NULL, LWP_TASK_STACK_SIZE, priority, tick); if (thread != RT_NULL) { struct rt_lwp *self_lwp; thread->tid = tid; lwp_tid_set_thread(tid, thread); LOG_D("lwp kernel => (0x%08x, 0x%08x)\n", (rt_size_t)thread->stack_addr, (rt_size_t)thread->stack_addr + thread->stack_size); level = rt_hw_interrupt_disable(); self_lwp = lwp_self(); rt_hw_interrupt_enable(level); rt_thread_startup(thread); return lwp_to_pid(lwp); } } lwp_tid_put(tid); lwp_ref_dec(lwp); return -RT_ERROR; } ``` 总结来说这个函数主要实现了如下功能 - 解析elf符号表 - 创建填充lwp结构体 - 创建一个线程,并在线程中运行elf内的_start地址。 ### 解析符号表 函数lwp_load->load_elf解析了用户态程序的符号表信息,并将他们拷贝到了对应的虚拟地址。符号表的含义是在编译程序工作的过程中需要不断收集、记录和使用源程序中一些语法符号的类型和特征等相关信息。 这些信息一般以表格形式存储于系统中。 如常数表、变量名表、数组名表、过程名表、标号表等等,统称为符号表。 ```c static int load_elf(int fd, int len, struct rt_lwp *lwp, uint8_t *load_addr, struct process_aux *aux) { lwp->text_entry = (void *)(eheader.e_entry + load_off); } ``` ELF文件内的ehdr中包含了[elf文件](https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html)的基本信息。 ```c typedef struct { unsigned char e_ident[EI_NIDENT]; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr; ``` 其中e_entry这个成员提供了系统最初传送控制的虚拟地址,从而启动进程。这里不做太细致的分析,整体结论是 1. 调用了load_elf之后,lwp->text_entry的值被设置为了e_entry的值。load_off这里的值为0,具体情况可以进一步研读代码。 ```c lwp->text_entry = (void *)(eheader.e_entry + load_off); ``` 2. 同时为代码段和数据段申请了内存空间 ```c /*in function load_elf*/ /* text and data */ for (i = 0; i < 2; i++) { if (user_area[i].size != 0) { va = lwp_map_user(lwp, user_area[i].start, user_area[i].size, (int)(i == 0)); if (!va || (va != user_area[i].start)) { result = -RT_ERROR; goto _exit; } } } ``` 3. 将代码段和数据段的内容读取到物理内存中,物理地址是上一个步骤的函数lwp_map_user申请的。 ```c uint32_t size = pheader.p_filesz; size_t tmp_len = 0; va = (void *)(pheader.p_vaddr + load_addr); read_len = 0; while (size) { pa = rt_hw_mmu_v2p(m_info, va); va_self = (void *)((char *)pa - PV_OFFSET); LOG_D("va_self = %p pa = %p", va_self, pa); tmp_len = (size < ARCH_PAGE_SIZE) ? size : ARCH_PAGE_SIZE; tmp_len = load_fread(va_self, 1, tmp_len, fd); rt_hw_cpu_dcache_ops(RT_HW_CACHE_FLUSH, va_self, tmp_len); read_len += tmp_len; size -= tmp_len; va = (void *)((char *)va + ARCH_PAGE_SIZE); } ``` 符号表解析到这里大概就结束了,程序被拷贝到了内存中,并且按照链接地址中提供的虚拟地址建立了页表映射。 ### 进程的第一个线程开始运行 lwp_load运行结束后,内核开始创建一个线程,线程名字被命名为elf文件的名字 ```c thread_name = strrchr(filename, '/'); thread_name = thread_name ? thread_name + 1 : filename; ``` 之后调用rt_thread_create创建一个线程。 ```c thread = rt_thread_create(thread_name, _lwp_thread_entry, RT_NULL, LWP_TASK_STACK_SIZE, priority, tick); ``` _lwp_thread_entry线程函数会负责运行进程代码段的起始地址 ```c tatic void _lwp_thread_entry(void *parameter) { rt_thread_t tid; struct rt_lwp *lwp; tid = rt_thread_self(); lwp = (struct rt_lwp *)tid->lwp; tid->cleanup = lwp_cleanup; tid->user_stack = RT_NULL; arch_start_umode(lwp->args, lwp->text_entry, (void *)USER_STACK_VEND, tid->stack_addr + tid->stack_size); } ``` arch_start_umode函数在rv64下是一个汇编函数 ```c /* * void arch_start_umode(args, text, ustack, kstack); */ .global arch_start_umode .type arch_start_umode, % function arch_start_umode: // load kstack for user process csrw sscratch, a3 li t0, SSTATUS_SPP | SSTATUS_SIE // set as user mode, close interrupt csrc sstatus, t0 li t0, SSTATUS_SPIE // enable interrupt when return to user mode csrs sstatus, t0 csrw sepc, a1 mv a3, a2 sret//enter user mode ``` - SCRATCH寄存器配置为了栈指针的值,该寄存器是超级用户模式异常临时数据备份寄存器(SSCRATCH)用于处理器在异常服务程序中备份临时数据。一般用来存储超级用户模式本地上下文空间的入口指针值。 - SEPC被设置为了a0也就是lwp->text_entry也就是我们链接脚本中_start对应的地址0x200000000。这个寄存器用于存储程序从异常服务程序退出时的程序计数器值(即 PC 值)。 之后调用sret返回用户模式,之所以sret是因为我们程序加载运行的最初入口是finish线程,这个线程是被按键输入中断唤醒的,这个线程是运行在内核态的,也就是说我们现在处在riscv处理器的S态,sret之后PC指针会指向0x200000000并将处理器的特权级别改为U态,从而运行用户态进程。 ### main函数的执行 分析到这里之后的代码就简单了,用户态的_start执行后,会先运行C库中的初始化函数为整个程序运行准备执行环境,例如对bss段进行清0操作。之后会固定跳转到main函数处运行。 ```bash 0000000200000000 <_start>: 200000000: 000001b7 lui gp,0x0 200000004: 00018193 mv gp,gp 200000008: 8132 mv sp,a2 20000000a: 00000593 li a1,0 20000000e: ff017113 andi sp,sp,-16 200000012: a009 j 200000014 <_start_c> 0000000200000014 <_start_c>: 200000014: 410c lw a1,0(a0) 200000016: 1141 addi sp,sp,-16 200000018: 00850613 addi a2,a0,8 20000001c: 4781 li a5,0 20000001e: 00583717 auipc a4,0x583 200000022: 8f670713 addi a4,a4,-1802 # 200582914 <_fini> 200000026: 00582697 auipc a3,0x582 20000002a: 50a68693 addi a3,a3,1290 # 200582530 <_init> 20000002e: 00001517 auipc a0,0x1 200000032: ba850513 addi a0,a0,-1112 # 200000bd6
200000036: e406 sd ra,8(sp) 200000038: 00582097 auipc ra,0x582 20000003c: 64e080e7 jalr 1614(ra) # 200582686 <__libc_start_main> ```
1
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
HAHABO
这家伙很懒,什么也没写!
文章
4
回答
1
被采纳
0
关注TA
发私信
相关文章
1
编译及下载必要进程的名称
2
RT-Studio支持把部分代码加载至RAM运行吗
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
rt_mq_消息队列_msg_queue
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
a1012112796
20
个答案
3
次被采纳
张世争
11
个答案
3
次被采纳
踩姑娘的小蘑菇
7
个答案
3
次被采纳
rv666
9
个答案
2
次被采纳
用户名由3_15位
13
个答案
1
次被采纳
本月文章贡献
程序员阿伟
9
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
RTT_逍遥
1
篇文章
6
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部