6.S081——Lab4——trap lab

news2024/11/17 21:22:28

0.briefly speaking

这是MIT 6.S081 Fall 2021课程的第四个实验,它是有关陷阱机制的一系列小问题,如果对陷阱机制仍有疑问,可以参考我之前写的其他3篇博客,它们很好地解释了一些背景知识

用户态陷阱(以系统调用为例)
内核态陷阱
RISC-V陷阱机制详解

本实验分为以下三个小任务,难度依次递增:

  • RISC-V assembly (easy)
  • Backtrace (moderate)
  • Alarm (hard)

下面我们一个个来研究一下…

1.RISC-V assembly (easy)

这个实验是一个学习性质的实验,设计这个实验本质上是想让我们回顾和熟悉一下RISC-V汇编语言和Calling Convention。这个实验就是阅读一些汇编语言程序并回答一些问题,我们一一来看看吧。

我们的研究的对象是一个叫做call.c的函数,它的内容非常简单,只有两个分别名为f和g的函数以及main函数,全部代码如下所示:

// g函数作用,给传入的值加3
int g(int x) {
  return x+3;
}

// f函数就是g函数的直接封装
int f(int x) {
  return g(x);
}

// 调用f函数,打印两个值
void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}

可以看到,main函数调用了f函数,f函数又是g函数的简单包装。按照实验指导书的说明,我们使用make fs.img指令可以编译这个C代码文件,并可以产生一个可读的汇编语言文件。基于此汇编代码,回答下述问题:

  • Q1:哪些寄存器用来存放函数所用的参数?比如在main函数中调用printf时13这个参数是在哪个寄存器中传递进去的?
  • A1:我们看看printf这段代码对应的汇编及其注释,如下所示:
 printf("%d %d\n", f(8)+1, 13);
 24:	4635              li	a2,13		# 将13放置到a2寄存器
 26:	45b1              li	a1,12		# 将12放置到a1寄存器
 28:	00000517          auipc	a0,0x0		# a0 = pc = 0x28
 2c:	7a050513          addi	a0,a0,1952 	# a0 = a0 + 1952 = 0x28 + 0x7a0 = 0x7c8
 30:	00000097          auipc	ra,0x0		# ra = pc = 0x30
 34:	5f8080e7          jalr	1528(ra) 	# pc = ra + 1528 = 0x30 + 0x5f8 = 0x628
 											# ra = pc + 4 = 0x38

我们可以看到13被放置到了a2寄存器中,而表示计算结果的f(8) + 1 = 8 + 3 + 1 = 12则被放置到了a1寄存器中,a0指向了一个地址7c8,可以想见这个地址应该是指向printf函数中第一个输出格式字符串的地址。上述的参数传递是符合RISC-V的calling convention的,事实上在传递整数参数时,如果参数个数少于8个,它们都会被放置在a0-a7中进行传递

  • Q2:main函数中对f函数调用的汇编代码在哪里?对g函数的调用又在哪里?(提示:编译器可以对函数进行内联)

  • A2:从上述的汇编代码中可以看到,编译器直接将对f函数的调用结果硬编码到了代码中,这样可以极大程度地减少函数调用过程的开销。

  • Q3:printf函数的地址是什么?

  • A3:从上述代码中可以看出,printf函数的地址就是最后一行汇编执行完成之后的PC的值,为0x628。

  • Q4:在main函数中执行完跳转到printf函数的jalr指令之后,ra寄存器的值是什么?

  • A4:上面我们已经计算出来了,ra寄存器的值应该是0x38,我们可以借这个机会熟悉一下GDB的用法,调试一下call.c这个用户程序,看看我们计算结果是否正确,调试的步骤如下,这也是在Xv6中调试用户态程序的基本方法,建议熟练掌握:

打开一个命令行终端,输入:make CPUS=1 qemu-gdb,打开gdb-server等待调试器连接。
在这里插入图片描述
打开另外一个命令行终端,输入调试器命令:gdb-multiarch(当然也可以是其他的调试器),如下所示:
在这里插入图片描述
然后在调试器一端,将我们要调试的用户态文件加载进来file user/_call,并在jalr指令处(虚拟地址是0x34)打一个断点,如下所示:
在这里插入图片描述
接下来,我们使用continue指令继续内核的启动过程,注意因为调试的是用户态程序call,只有在进入操作系统之后执行这个程序才可以出发上述的断点。我们输入continue,切换到GDB服务器端,可以看到内核已经打印出了一些信息,并且GDB显示已经触发了断点:
在这里插入图片描述
在这里插入图片描述
千万注意,这时候触发的断点并不是call.c这个程序中的,而是在启动内核的某个瞬间PC恰好等于了0x34这个地址值,GDB就触发了这个断点。GDB笨笨的,它只会检查当前PC值是否等于你设定的断点位置,但它并不管这是哪段程序中的。我们可以使用disas指令看看当前上下文的汇编代码:
在这里插入图片描述
可以看到当前执行的汇编代码和call.c中的printf的反汇编完全不一致,所以现在触发的这个所谓断点并不是位于call.c中的,我们直接用continue指令跳过这个断点,于是内核这时完全启动成功了,这时我们在终端中执行一下call程序,可以看到它被断点阻塞住了
在这里插入图片描述
回到GDB一端,可以看到再次触发了我们的断点,这次是真的进入到我们的程序中了,用disas查看一下反汇编,发现和call.asm中的代码完全一致:
在这里插入图片描述
这次是真的阻塞在了我们期待的地方,于是我们输入stepi指令,让其向前执行一条汇编,此时控制流将转向printf函数,同时ra寄存器会完成设置:
在这里插入图片描述
我们看看当前ra寄存器的值即可,使用info registers ra指令,它的值是0x38,和我们计算出的值是一致的,猜想正确
在这里插入图片描述
跑了个很大的题,不过我认为它是值得的,哈哈,让我们重回正轨。

  • Q5<: 执行下面的代码会输出什么?
    在这里插入图片描述
    输出基于这样一个事实,RISC-V是一个基于小端的(little-endian),如果RISC-V是基于大端的那么应该如何修改i的值来产生同样的输出?你需要将57616修改为不同的值吗?

  • A5:首先将这段代码写入call.c,编译运行一下,结果如下:
    在这里插入图片描述
    输出了一个"He110 World",其中e110是57616的16进制表示,而rld是无符号整数i中每个字节的对应字符。如果现在RISC-V存储改为大端,i的值就应该初始化为0x00726c46了,而57616无需修改,因为大小端存放并不会改变它转化为16进制数之后的结果

  • Q6:在下面的代码中,y= 之后会打印出什么?(此值不确定),这是为什么?
    在这里插入图片描述

  • A6:我们将call.c文件中的main函数替换为上述代码,编译执行,结果如下:
    在这里插入图片描述
    首先上述行为是没有定义的行为(Undefined Behavior),因为printf中传入的参数数量少于格式化字符串中要求的数量,我不打算在此详细介绍前因后果(涉及到va_list等等,而va_list其实是一个指针,指向一块连续的内存区域),写出来篇幅会很长。我使用GDB调试了上述程序,发现5309其实是紧跟在3之后的一块未初始化的内存数据,这就是问题的答案。

2.Backtrace (moderate)

这个实验让我们实现一个backtrace函数,可以打印函数调用栈,从而方便我们进行debug。这个小任务其实不难,最重要的一个前备知识是了解并熟悉Xv6中的函数调用栈帧结构(stack frame),知道了各个寄存器的相对存放位置,这个任务就十分简单了。

首先按照实验指导书的要求,将一个读取fp寄存器的嵌入式汇编定义到riscv.h文件中:

/* read frame pointer*/
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

下面简单绘制一下Xv6内核中的栈帧结构(stack frame),这对后面的代码实现非常重要。当我们在使用Frame Pointer时,这个寄存器其实指向的是上一级栈帧的最后一个位置,这是因为RISC-V中栈指针(stack pointer)是遵循满递减原则的。那么FP-8一定存放的是上一级函数的返回地址ra,FP-16这个地址一定存放的是上一级栈帧指针Previous FP,这是我们实现回溯的重要条件。
在这里插入图片描述
知道了栈帧的基本结构之后,我们会发现这样的多层嵌套调用的函数的栈帧本质上构成了一个链表,链表的入口就是当前存放在FP寄存器中的值,链表的next指针就是Previous FP指针(FP-16)。最后就是回溯的终点,我们知道链表的终点一般是一个空指针(nullptr),这里也是一样的,RISC-V abi中明确规定了FP链表的最后一个Previous FP位置的值为0(The end of the frame record chain is indicated by the address zero appearing as the next link in the chain)。但我们在实验中没有这么做,Xv6的内核实现中整个栈只有一页(4K)大小,所以我们完全可以通过地址值去检测是否回溯到了最后一个栈帧

最后,给出完整的代码实现:

/* backtrace : print the call stack of function */
void backtrace()
{
  // 读出当前FP指针的值,使用上面添加的嵌入式汇编函数
  // 根据FP指针的值计算出栈底位置,这将作为循环结束的条件
  uint64 FramePointer = r_fp();
  uint64 KernelTop = PGROUNDDOWN(FramePointer) + PGSIZE;
  printf("backtrace:\n");
  
  // 如果没有到最后一级,则持续向前一级回溯
  while(FramePointer < KernelTop)
  {
  	// 从FP-8这个位置取出上一级函数返回地址,打印出来
    uint64 ReturnAddr = *(uint64*)(FramePointer - 8);
    printf("%p\n", ReturnAddr);
	
	// 回溯到上一层函数栈帧
    FramePointer = *(uint64*)(FramePointer - 16);
  }
  return;
}

我们将这个函数加入到sys_sleep函数的实现中,测试一下:

uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  if(argint(0, &n) < 0)
    return -1;
  acquire(&tickslock);
  ticks0 = ticks;
  while(ticks - ticks0 < n){
    if(myproc()->killed){
      release(&tickslock);
      return -1;
    }
    sleep(&ticks, &tickslock);
  }
  release(&tickslock);
  backtrace();          // test backtrace here
  return 0;
}

启动内核,执行bttest,结果如下:
在这里插入图片描述
再使用虚拟地址到代码行数的映射addr2line转换一下,结果如下:
在这里插入图片描述
结果是正确的,它们的确指向对应函数的返回地址,并且也可以通过测试程序。现在我们多了一个debug的利器,我们按照指导书上的提示,将其加入panic函数,以备未来debug时使用:)。最后我们执行一下测试程序来判断一下程序的正确性,结果如下:
在这里插入图片描述

最后提一下,使用所谓的地址去检测是否回溯到栈顶的做法是非常危险的。我使用GDB去调试了上述程序,发现在跳出循环时FP指针正好和栈底的地址保持一致(故代码实现中地址比较必须是<号,<=号就会酿成大祸),而此时Previous FP是一个极小的值:0x2fe0,这个值是在用户陷入内核态执行系统调用时残存在FP寄存器中的值,它指向一个很小的地址,这个地址本质上指向了一个原先用户态的虚拟地址。如果循此地址继续下去,将会陷入一个死循环中,这是本实验外的一些注意点。
在这里插入图片描述

3.Alarm (hard)

这是本次实验的最后一个小任务,也有一些难度。这个实验的目标是让我们实现两个系统调用sigalarm和sigreturn,实现一个进程执行时定时打断转去执行其他函数之后返回的功能。在实验指导书上有较为详尽的说明,它分为了两个部分,分别实现sigalarm和sigreturn。因为这是一个完整的故事,我并不打算将它拆成两个部分来讲,因为这样会破坏故事的完整性。我将尝试一以贯之地把整个调用过程描述出来:)

在开始本任务的代码实现之前,你可能需要回顾一下如何添加一个系统调用,这在我们之前的博客中已经有了总结,详见6.S081——Lab2——system calls中的Briefly Speaking部分,它总结了添加一个系统调用的所有步骤,我们在下面不再对这些步骤做一一描述,而只关注问题本身的逻辑。好的,我们接下来就开始了!

我想首先给出一张完整的概览图,阐述这个任务完整的调用轨迹,如下所示,其实这张图将我们要做的事情都总结的差不多了,现在只需要将其中的细节用代码实现出来即可,在下面的讲解中我会用图中的数字来索引对应的动作,所以这张图是非常有意义的:

在这里插入图片描述
第一个小目标是实现系统调用sigalarm,这个系统调用可以设置一个进程被时钟打断的间隔(Inteval)和响应函数(handler),一经设定,这个进程就会以Inteval的时间间隔调用处理函数handler,从而进入上图的完整调用流程。因为时间间隔和调用函数handler都是与进程状态强相关的,所以我们将其保存在struct proc中,经过修改之后的proc结构体如下(新添加的域都以尖括号的形式注释):

struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct trapframe alarmframe; // <在发生sigalarm调用时,用于保存之前的trapframe>
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  int AlarmInteval;            // <触发alarm handler的时间间隔>
  int  Counter;                // <计时器,记录从上次触发handler到现在经过的ticks数量>
  char InHandler;              // <标志位,表示当前进程是否处于响应alarm的流程中>
  uint64 Handler;              // <handler在用户态的虚拟地址>
};

所以其实sigalarm在内核的实现sys_sigalarm非常简单,只是将用户传进来的参数记录到进程结构体中即可,所以它的实现如下所示:

// sigalarm对应的内核代码实现,只需要将用户传入的参数
uint64 
sys_sigalarm(void)
{
  if(argint(0, &myproc()->AlarmInteval) < 0)
    return -1;
  if(argaddr(1, &myproc()->Handler) < 0)
    return -1;
  return 0;
}

在内核中实现了sigalarm系统调用之后,用户可能会在它的某个程序执行期间调用sigalarm系统调用,从而完成中断间隔时间和中断处理函数在内核的注册。一经注册,内核就开始对时间间隔进行统计,并开始周期性地调用这个中断处理函数了。

如同图中的①所示,一个进程在执行时,定时器可能会在其中某个不确定的时刻触发一个时钟中断。这时候将会进入系统调用的相应流程,首先经由trampoline进入usertrap函数,也就是上图中的②,在这里我们将对这个时钟中断进行处理,并适时地进行alarm响应过程,首先给出代码和注释:

void
usertrap(void)
{
  // 以上代码从略...
  if(p->killed)
    exit(-1);
   
  // 如果是时钟中断,且当前开启了alarm功能并且没有处于其他响应过程中,则开始处理中断
  // p->AlarmInteval == 0表示我们当前关闭了alarm,因此不要响应
  // p->InHandler == 0表示当前进程没有在处理alarm流程中,可以进行下一次响应
  if(which_dev ==  2 && p->AlarmInteval != 0 && p->InHandler == 0)     
  {
    // 记录当前已经经过的ticks总数,计数器加一
    p->Counter ++;             
	
	// 如果到了应该触发handler的间隔时间         
    if(p->Counter == p->AlarmInteval)                                   
    {
      // 首先将trapframe完整保存在proc的alarmframe中
      // 这是为了以后可以不受影响地回到原有进程中执行
      // 出于便利,我选择直接将trapframe中的所有信息保存下来
      memmove(&p->alarmframe, p->trapframe, 
              sizeof(struct trapframe));                                
      
      // 修改trapframe中的epc,使得陷阱将会返回到用户态下的handler函数中
      p->trapframe->epc = p->Handler;                                  
      // 设置标志位,表明当前进程正处于alarm的处理流程中,不再响应其他alarm
      p->InHandler = 1;                                                
    }
  }
// 以下代码从略...
}

所以这就是我对usertrap的修改逻辑,这里做的事情就是记录经过的时间,如果时间达到了我们设定的间隔,则触发alarm响应过程。首先将trapframe完全保存,然后设置trapframe中的epc寄存器的值设置为handler的虚拟地址,使其可以经由usertrapret返回到用户态下的handler函数中去。别忘了还要设置标志位InHandler,这可以避免重入(re-enter)handler而造成的错误。

经过我们的设置,现在回到了上图中的③,在这里会执行一些用户定义的动作,并在最后执行sigreturn系统调用,于是操作系统再次陷入内核态,经过usertrap的转发(上图中的④)之后进入内核的sys_sigreturn系统调用,sys_sigreturn的实现非常简单,只是简单的恢复现场并重置计数器,如上图中的⑤所示,给出我的代码实现和注释:

uint64
sys_sigreturn(void)
{
  struct proc* p = myproc();
  
  // 恢复现场并将计数器和标志重置
  memmove(p->trapframe, &p->alarmframe, sizeof(struct trapframe));  
  p->Counter = 0;                                                   
  p->InHandler = 0;                                                   
  return 0;
}

在这里我们恢复了最早程序在执行用户态程序时的现场,并将计时器和标志全部初始化,这样进程就可以成果响应下一次alarm了:),接下来经过usertrapret的恢复,现在进程又回到了最早的程序中,且现场没有受到任何影响,如图中的⑥所示。

这就是整个alarm任务的完整实现思路和解释,其实看起来也并不复杂:)

我执行了alarmtest来测试实现的正确性,结果如下,说明实现没有问题:
在这里插入图片描述
usertests我这里就不再展示结果了,它测试的是你加入的代码是否对内核完整性造成了损失,测试的时间比较长,我测试过程序没有问题。这里只给出最后的测试程序报告,没有问题:
在这里插入图片描述
本实验至此终,哈哈哈!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/654763.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

oracle 19c 部署

安装前的基础环境和用户配置等参考rac部署篇oracle rac部署 一、资源准备 将数据库软件上传解压到oracle的家目录(注意解压后的用户属组) [oraclerac1 ~]$ unzip -d $ORACLE_HOME xxxx.zip 二、在xmanager或者vnc中执行安装 [oraclerac1 db_1]$ ./runInstaller 先安装一个数据…

SSM 框架常见面试题

1 Spring面试题 1、Spring 在ssm中起什么作用&#xff1f; Spring&#xff1a;轻量级框架 作用&#xff1a;Bean工厂&#xff0c;用来管理Bean的生命周期和框架集成。 两大核心&#xff1a;1、IOC/DI(控制反转/依赖注入) &#xff1a;把dao依赖注入到service层&#xff0c;ser…

5.3、Dockerfile内命令

【docker】CMD ENTRYPOINT 区别 终极解读&#xff01;_绝世好阿狸的博客-CSDN博客 0、上下文路径 $ docker build -t nginx:v3 . # . 是上下文路径&#xff0c;那么什么是上下文路径呢&#xff1f; 上下文路径&#xff1a;指docker在构建镜像时想使用本机的文件&#xff0c;…

缓冲区溢出与防护

目录 一、初识缓冲区溢出 1.1 缓冲区溢出概念 1.2 缓存区 1.3 缓存区溢出的危害 1.4 缓存区溢出事件 二、缓存区溢出攻击 2.1 溢出原理 2.2 典型的寄存器 三、缓存区溢出防御 3.1 缓冲区溢出攻击目标 3.2 缓冲区溢出条件 3.3 缓冲区溢出防范 3.3.1 程序设计过程中…

【后端开发】狂神笔记:Redis进阶

文章目录 1 Redis事务1.1 Redis事务简介1.2 Redis事务操作过程1.2.1 开启事务--->执行事务1.2.2 取消事务(discurd) 1.3 事务错误1.3.1 编译期异常1.3.2 运行时异常 2 Redis实现乐观锁2.1 乐观锁和悲观锁2.2 正常执行2.3 测试异常执行 3 Jedis3.1 导入依赖3.2 编码测试3.2.1…

青翼科技自研模块化互联产品 • 模拟采集FMC子卡【产品资料】

FMC122是一款基于FMC标准规范&#xff0c;实现2路16-bit、1GSPS ADC同步采集&#xff0c;2路16-bit 2.5GSPS DAC同步回放功能子卡模块。该模块遵循VITA57.1标准&#xff0c;可直接与FPGA载卡配合使用&#xff0c;板卡ADC器件采用TI的ADS54J60芯片&#xff0c;该芯片具有两个模拟…

EasyExcel实现execl导入导出

引言 在实际开发中&#xff0c;处理 Excel 文件是一个常见的需求。EasyExcel 是一个基于 Java 的开源库&#xff0c;提供了简单易用的 API&#xff0c;可以方便地读取和写入 Excel 文件。本文将介绍如何使用 EasyExcel 实现 Excel 导入功能&#xff0c;以及一些相关的技巧和注…

java的线程池

一、线程池概念 若不使用线程池&#xff0c;在新创建的线程start()后执行完run()就自动销毁了&#xff0c;造成了资源的浪费。使用线程池可以暂时存储线程。 线程池的主要核心原理&#xff1a; 线程池的代码实现&#xff1a; 注意&#xff1a;在实际开发中线程池并不会关闭&a…

【MySQL】一文带你了解排序检索数据

&#x1f3ac; 博客主页&#xff1a;博主链接 &#x1f3a5; 本文由 M malloc 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;LeetCode刷题集&#xff01; &#x1f3c5; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指…

经典电路分析——看看大佬设计或代代人改进的作品——欢迎批评指正

1、TTL反相器 1&#xff09;输入0.3v a)T1&#xff0c;T2&#xff0c;T4 先分析T1的工作状态&#xff1a;计算ube1和uce1 ube1大于开启电压&#xff0c;所以ube10.7v。 因为T2&#xff0c;T4要想导通&#xff0c;V1b上需要有打通三个PN结的能力&#xff0c;也就是说V1b3*0.…

【UE 从零开始制作坦克】3-履带移动效果

目录 一、设置转向灵敏度 二、原地掉头 三、履带移动效果 效果 一、设置转向灵敏度 为了解决坦克转向灵敏度过高的问题&#xff0c;我们首先打开“TankZongLei”蓝图 选中“载具移动&#xff08;MovementComp&#xff09;&#xff08;继承&#xff09;”将转向输入率中的上…

数据结构图解--队列 的实现/算法+源码

图解--队列 最终的效果接下来 我们要做的就是 使用编程实现这种意识形态下的结构创建队列结构体初始化init进队入队 尾部插入队列的判断满操作出队队列是否为空遍历队列代码的操作交互意识 最终的效果 1.队列的结构形式 队列是一种线性结构 但是特殊的线性结构 只要的特殊在于…

【炫酷登录界面】详解5款高级的前端登录页面及实现源码(附完整源码)

【写在前面】 其实好早之前我就想写这篇文章了&#xff0c;也有些网友还会私信我&#xff0c;说有时候公司要求登录页面的改造&#xff0c;问我能不能出一期关于登录页的文章&#xff0c;于是乎我也是拖到这个时候才整理出来&#xff0c;其实每篇文章的效果内容我都是自己亲自去…

基于51单片机设计的花样流水灯设计

一、项目介绍 花样流水灯是一种常见的LED灯效果,被广泛应用于舞台表演、节日庆典、晚会演出等场合。在现代智能家居、电子产品中,花样流水灯也被广泛使用,通过调整亮灭顺序和时间,可以实现各种炫酷的灯光效果,增强用户体验。而51单片机作为一种常见的嵌入式开发平台,具有…

深搜-剪枝优化

目录 1.问题引入 2.知识讲解 【搜索术语】 &#xff08;1&#xff09; *可行性剪枝*&#xff1a; &#xff08;2&#xff09; *预处理剪枝*&#xff1a; &#xff08;3&#xff09; *重复性剪枝*&#xff1a; &#xff08;4&#xff09; *最优性剪枝*&#xff1a; &…

动态规划IV (118、119、198、213、337)

CP118 杨辉三角 题目描述&#xff1a; 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 学习记录&#xff1a; 思想就是没有思想&#xff0c;的杨辉三角&#xff0c;但是注意resiz…

AUTOSAR MCAL之SPI(Specification of SPI Handler/Driver)

本文将详细介绍AUTOSAR MCAL SPI模块的知识点及注意事项&#xff0c;本模块的配置会在其他文章进行分享。本文大部分内容来源于标准&#xff0c;并参照了NXP S32K1系列的 MCAL SPI的代码。 耐心看完本文后&#xff0c;你就对AUTOSAR MCAL SPI有了非常深入的了解。 目录 1. 模…

在githhub上创建个人主页的方法【2023更新版】

01-进入github的网站&#xff0c;链接 https://github.com/ &#xff0c;然后注册&#xff0c;登陆&#xff0c;注意登陆时设置的用户名(username)就是将来你个人主页的三级域名&#xff0c;所以这里一定要慎重填写username。如下图所示&#xff1a; 02-注册完成后进入个人主…

【备战秋招】每日一题:5月13日美团春招第四题:题面+题目思路 + C++/python/js/Go/java带注释

为了更好的阅读体检&#xff0c;为了更好的阅读体检&#xff0c;&#xff0c;可以查看我的算法学习博客第四题-沙堡 在线评测链接:P1289 题目描述 塔子哥在海边建了一个沙堡乐园。 里面有一个巨大的沙堡&#xff0c;塔子哥每年都会增加这个沙堡的层数&#xff0c;但也有一定…

Found a swap file by the name “.credentials.swp“

问题 使用vim终端编辑一个文件的时候&#xff0c;遇到了一个提示&#xff1a;Found a swap file by the name ".credentials.swp" 解决 这是由于编辑文件意外退出导致的&#xff0c;产生了.swp文件,这个时候&#xff0c;只要按下键盘【A】键 然后&#xff0c;使用…