MIT6.s081 2021 Lab Traps

使用gdb调试xv6内核

从最近两个 Lab 开始,代码逻辑的复杂度明显上升,对内核进行调试可能是帮助理解操作系统机制的绝佳方法。因此在开始本 Lab 之前,我们先来配置一下针对 xv6 内核的 gdb 调试器。

  1. 安装 gdb-multiarch.

利用包管理工具进行安装,我使用的是 Ubuntu 系统,执行以下命令:

sudo apt install gdb-multiarch
  1. 在 xv6 项目根目录下可以看到 .gdbinit 文件,其中已经写好了一些 gdb 的初始化选项,使用文本编辑器或 cat 命令查看:
set confirm off                                                         
set architecture riscv:rv64                                             
target remote 127.0.0.1:26000                                           
symbol-file kernel/kernel                                               
set disassemble-next-line auto           
set riscv use-compressed-breakpoints yes
  1. ~/.config/gdb/ 目录下的文件 gdbinit 中(没有则新建)添加安全加载路径,否则可能无法加载 .gdbinit 的配置。
add-auto-load-safe-path <xv6项目的根目录>/.gdbinit
  1. 打开两个终端窗口(可以使用 tmux 进行分屏),都需要进入 xv6 根目录,第一个窗口输入 make-qemu 等待调试器连接,第二个窗口输入 gdb-multiarch 打开 gdb,如果前面配置正确,那么 gdb 并自动加载 .gdbinit 配置,与 qemu 连接,之后便可以开始正常调试了。

在这里插入图片描述

RISC-V assembly

一些有关 RISC-V 汇编的问题,最好先通过网上博客或手册简单了解一下 RISC-V 的基本指令。

Q1:

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

A1:

可以参考 RISC-V 的 calling conventiona0 - a7: 这些寄存器用于传递函数的前八个整数或指针类型的参数,如果超出这些寄存器的数量,超出的部分会存放在栈上。观察指令 li a2,13 可知,13 作为 printf 的第二个参数,存放在寄存器 a2 中。

在这里插入图片描述

Q2:

Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

A2:

调用函数 f 和函数 g 的代码被编译器优化,直接计算出了结果 12,作为 printf 的参数存入寄存器 a1 中:

26:   45b1                    li  a1,12

Q3:

At what address is the function printf located?

A3:

位于 0x638 地址处。

Q4:

What value is in the register ra just after the jalr to printf in main?

A4: 参考 riscv-calling,ra 用来存储函数调用的返回地址,因此 ra 的值为 jalr 1544(ra) 的后一条指令地址,即 0x38.

Q5:

Run the following code.

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

What is the output? Here’s an ASCII table that maps bytes to characters.

The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

Here’s a description of little- and big-endian and a more whimsical description.

A5:

  • %x 用于输出一个无符号十六进制整数。
  • %s 用于输出一个字符指针所指向的字符串,直到遇到空字符\0为止。

小端模式下,57616 的 十六进制表示为 e110,&i 首地址开始的字节分别为 0x72, 0x6c, 0x64, 0x0,对应 ASCII 表中的字符为 r, l, d,因此最终输出结果为 He110 World.

若采取大端模式,i 的值应当替换为 0x726c6400,57616 的值无需改变,因为十六进制的书写规则并没有改变(高位在左,低位在右)。

Q6:

In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?

printf("x=%d y=%d", 3);

A6:

关于可变参数的内容查看 《C Programming Language 2nd Edition》(K&R)的 7.3 节 Variable-length Argument Lists.

简而言之,这样的操作将引发未定义行为,此时 ap 指向了一个未知的内存区域,并将该区域的数据以整型的形式输出。

Backtrace

思路

思路其实很简单:对照 lecture notes 给出的栈的结构,从当前栈帧的起始地址 fp 开始,fp - 8 的位置存放着当前函数调用的返回地址(上一次函数调用处的下一条指令地址),即我们 需要打印 的地址,fp - 16 的位置存放着上一次函数调用所在栈帧的起始地址,将该地址作为新的 fp 重复上述步骤即可。

在这里插入图片描述

关键问题是 什么时候停止 ?可以看到上述 backtrace 的过程就好像是在遍历一个链表,当链表的 next 域为空指针时链表到达末尾,那 traceback 完成后fp 的值应该是什么?为了寻找这个问题的答案,我选择先不设置终止条件,让它一直向上搜索,最后发现,返回地址最终为一个很小的值,这个地址显然不是我们想要的,在此之前应该退出,即本次 traceback 的尽头是 0x80001c92.

在这里插入图片描述

但打印出来的函数调用的返回地址似乎并没有什么规律,因此我又尝试将遍历过程中的栈帧起始地址 fp 打印出来,得到以下结果:

在这里插入图片描述

结合提示:

Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address.

原因就很明显了,在打印第三个返回地址时,此时栈帧起始地址为 0x3fffffa000,注意该地址后 12 二进制数为 0,且页面大小为 4KB,因此该地址位于一个页面的起始地址。又因为 xv6 内核只为每个 内核栈 分配一个页面的存储空间,该页面的起始地址按页面大小对齐,所以此时已经到达一个内核栈的顶端,无需继续遍历。

弄清楚了这些,代码的编写就很简单了:

void backtrace(void) {printf("backtrace:\n");uint64 fp = r_fp();uint64 top = PGROUNDUP(fp);do {printf("%p\n", *(uint64 *)(fp - 8));fp = *(uint64 *)(fp - 16);} while (fp < top); // reach the top of kernel stack
}

代码

--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -80,6 +80,7 @@ int             pipewrite(struct pipe*, uint64, int);void            printf(char*, ...);void            panic(char*) __attribute__((noreturn));void            printfinit(void);
+void			backtrace(void); // here// proc.cint             cpuid(void);
diff --git a/kernel/printf.c b/kernel/printf.c
index e1347de..a068cbd 100644
--- a/kernel/printf.c
+++ b/kernel/printf.c
@@ -114,6 +114,23 @@ printf(char *fmt, ...)release(&pr.lock);}+// here
+void backtrace(void) {
+	printf("backtrace:\n");
+	uint64 fp = r_fp();
+	uint64 top = PGROUNDUP(fp);
+
+	do {
+		printf("%p\n", *(uint64 *)(fp - 8));
+		fp = *(uint64 *)(fp - 16);
+	} while (lower < top);
+}
+voidpanic(char *s){
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 1691faf..fae7bf3 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -331,6 +331,15 @@ sfence_vma()asm volatile("sfence.vma zero, zero");}+// here
+static inline uint64
+r_fp()
+{
+  uint64 x;
+  asm volatile("mv %0, s0" : "=r" (x) );
+  return x;
+}
+#define PGSIZE 4096 // bytes per page#define PGSHIFT 12  // bits of offset within a page
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index e8bcda9..f27c007 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -70,6 +70,7 @@ sys_sleep(void)sleep(&ticks, &tickslock);}release(&tickslock);
+  backtrace(); // herereturn 0;}

Alarm

思路

目前为止感觉最复杂的一题,需要对 trap 机制有一个比较深入的理解,建议在上手之前先仔细阅读与 trap 有关的代码:kernel/trampoline.Skernel/trap.c,这里也推荐一位博主写的两篇有关 xv6 的 trap 机制的博客:

6.S081——陷阱部分(一文读懂xv6系统调用)——xv6源码完全解析系列(5)

6.S081——补充材料——RISC-V架构中的异常与中断详解

test0: invoke handler

我们不妨按照提示的顺序来进行,不关注 sys_sigreturn,先把 sys_sigalarm 的功能实现。

实际上,sys_sigalarm 函数的功能很简单,只是简单地将用户态下传递的参数 tickshandler 存入进程的 struct proc 结构体中。实现调用 handler 的操作需要在内核态下的 usertrap 中完成,具体来说,针对时钟中断导致的 trap 将在 if(which_dev == 2) 后的语句中被处理。有两个目标需要完成: 定时函数调用

定时的逻辑比较清楚,在 struct proc 中添加变量 ticksum,代表从上次 handler 处理完成开始进程累计的时钟中断次数,该变量在进程初始化时设置为 0,随后每次遇到时钟中断,都自增 1,如果自增后的值达到了设定的间隔 ticks,则将其复位为 0,调用 handler 函数。

函数调用是一个需要考虑的问题,这里不能直接利用函数指针 handler 进行函数调用,因为 handler 指向的函数位于用户空间下,而 usertrap 位于内核态下,页表的地址映射不同,无法直接根据用户空间下的虚拟地址进行寻址(直接调用引发的错误如下图所示),需要在本次中断结束返回到用户态之后执行。因此正确的做法应该是设置进程 struct procepc 寄存器为函数指针 handler,这样在中断处理完成,进程回到用户态并被 CPU 调度执行后,寄存器 pc 将被设置预先保存的 epc 的值,这样函数 handler 就被成功调度执行了。至此,test0 应该成功通过。

在这里插入图片描述

在进入到 test1&2 之前,有必要说一说我的一些思考:在上面的讨论中,我们知道内核无法直接根据函数指针 handler 的值进行用户空间函数的调用,那能否在内核态下根据进程的用户态页表和给定的虚拟地址,利用软件地址转换机制(vm.c 中的 walkaddr 函数)来将用户空间的虚拟地址转换为物理地址进行寻址呢(这也是我最开始的想法)?答案是不行,因为即便是在内核态下,程序中的地址仍然是虚拟地址,也就是说即便知道用户态函数实际存储的物理地址,我们也只有在 给出一个虚拟地址,该虚拟地址经过内核页表地址转换之后,刚好得到了正确的物理地址, 才可能成功。而实际上,尽管内核 KERNBASEPHYSTOP 地址都是直接映射,但内核页表中可能并没有所需要的页表项,因此,这并不会成功。

test1/test2(): resume interrupted code

test1 的目标是,存储和恢复中断处理前后的寄存器状态。那么问题就来了:为什么需要存储这些寄存器?需要存储哪些寄存器?

其实最开始,我是有些纠结寄存器状态的存储目的是什么,认为可能是与内核态和用户态切换有关,但仔细想想,这部分的工作应该是由 trampoline.Susertrapret 来完成的,那么为什么还需要存储和恢复寄存器?

事实上,在系统未关闭中断的情况下,时钟中断可能在程序执行的任何时刻发生,且在返回到原程序位置继续执行之前还需要执行预先设定好的 handler 函数,那么寄存器状态的保存将是必要的。一方面在执行 handler 函数期间,如果 handler 函数包含一些对局部变量的处理,那么通用寄存器的值将会发生改变,从而使得中断返回时程序的执行结果与预期不符;另一方面,由于 epc 的值被手动改变,如果执行完 handler 之后不恢复中断发生时的保存的 pc 值,那么 pc 将会指向 handler 函数末尾的下一条指令,中断因此无法正常返回。 简单来说,这部分的操作相当于手动模拟了 线程 的切换。

另一个问题是:需要存储哪些寄存器?好吧,在解决这个 Lab 时我其实偷了点懒,没有去仔细琢磨,只是简单地将整个 trapframe 中所有的寄存器都保存下来。但根据上面的讨论,再结合 RISC-V 的 calling convention,应该不难得出答案。

最后的 test2 就比较简单了,目标是:

Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again.

解决的办法有很多,可以额外在 strcut proc 添加一个变量,用来表示进程当前是否正处在处理 handler 的过程中,如果是,则不进行 ticksum 的自增操作。这里我采用了一点 小技巧 :不添加额外的变量,而是在处理 handler 前将 ticksum 置为负数,并在自增前判断 ticksum 是否非负,在 sys_sigreturn 时再将它置为 0,本质上与添加变量的操作大差不差。

代码

diff --git a/Makefile b/Makefile
index 7a7e380..bc4d47a 100644
--- a/Makefile
+++ b/Makefile
@@ -188,6 +188,7 @@ UPROGS=\$U/_grind\$U/_wc\$U/_zombie\
+	$U/_alarmtest\diff --git a/kernel/proc.c b/kernel/proc.c
index 22e7ce4..80096f7 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -119,6 +119,7 @@ allocproc(void)found:p->pid = allocpid();p->state = USED;
+  p->ticksum = 0; // here// Allocate a trapframe page.if((p->trapframe = (struct trapframe *)kalloc()) == 0){
diff --git a/kernel/proc.h b/kernel/proc.h
index f6ca8b7..c1d5a23 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -105,4 +105,10 @@ struct proc {struct file *ofile[NOFILE];  // Open filesstruct inode *cwd;           // Current directorychar name[16];               // Process name (debugging)
+
+  int ticks;         // here
+  void (*handler)();
+  int ticksum;
+
+  struct trapframe strapframe;};
diff --git a/kernel/syscall.c b/kernel/syscall.c
index c1b3670..d4e5585 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -104,6 +104,8 @@ extern uint64 sys_unlink(void);extern uint64 sys_wait(void);extern uint64 sys_write(void);extern uint64 sys_uptime(void);
+extern uint64 sys_sigalarm(void); // here
+extern uint64 sys_sigreturn(void);static uint64 (*syscalls[])(void) = {[SYS_fork]    sys_fork,
@@ -127,6 +129,8 @@ static uint64 (*syscalls[])(void) = {[SYS_link]    sys_link,[SYS_mkdir]   sys_mkdir,[SYS_close]   sys_close,
+[SYS_sigalarm]  sys_sigalarm,  // here
+[SYS_sigreturn] sys_sigreturn,};void
diff --git a/kernel/syscall.h b/kernel/syscall.h
index bc5f356..a040610 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -20,3 +20,5 @@#define SYS_link   19#define SYS_mkdir  20#define SYS_close  21
+#define SYS_sigalarm  22  // here
+#define SYS_sigreturn 23
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index f27c007..ee859ed 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -96,3 +96,28 @@ sys_uptime(void)release(&tickslock);return xticks;}
+
+// here
+uint64 sys_sigalarm(void) {
+	struct proc *p = myproc();
+
+	if (argint(0, &(p->ticks)) < 0) {
+		return -1;
+	}
+
+	if (argaddr(1, (uint64 *)&(p->handler)) < 0) {
+		return -1;
+	}
+
+	return 0;
+}
+
+uint64 sys_sigreturn(void) {
+	struct proc *p = myproc();
+	
+	// restore registers
+	memmove(p->trapframe, &(p->strapframe), sizeof(p->strapframe));
+
+	p->ticksum = 0;
+	return 0;
+}
diff --git a/kernel/trap.c b/kernel/trap.c
index a63249e..447e6d8 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c@@ -77,8 +77,17 @@ usertrap(void)exit(-1);// give up the CPU if this is a timer interrupt.
-  if(which_dev == 2)
+  if(which_dev == 2) {
+	// here
+	if (p->ticks > 0 && p->ticksum >= 0 && ++(p->ticksum) >= p->ticks) {
+	  // save registers
+	  memmove(&(p->strapframe), p->trapframe, sizeof(p->strapframe));
+
+	  p->ticksum = -1; // prevent re-entrant calls to the handler
+	  p->trapframe->epc = (uint64)p->handler;
+	}yield();
+  }usertrapret();}
diff --git a/user/user.h b/user/user.h
index b71ecda..422a4c1 100644
--- a/user/user.h
+++ b/user/user.h
@@ -23,6 +23,8 @@ int getpid(void);char* sbrk(int);int sleep(int);int uptime(void);
+int sigalarm(int ticks, void (*handler)()); // here
+int sigreturn(void);// ulib.cint stat(const char*, struct stat*);
diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..84c6784 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,5 @@ entry("getpid");entry("sbrk");entry("sleep");entry("uptime");
+entry("sigalarm"); # here
+entry("sigreturn");

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

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

相关文章

基于Maximin的异常检测方法(MATLAB)

异常存在于各个应用领域之中&#xff0c;往往比正常所携带的信息更多也更为重要。例如医疗系统中疾病模式&#xff0c;信用卡消费中的欺诈行为&#xff0c;数据库中数据泄露&#xff0c;大型机器故障&#xff0c;网络入侵行为等。大数据技术体系的快速兴起与发展&#xff0c;加…

【React Native优质开源项目】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

字符串——string类的常用接口

一、string类对象的常见构造 二、string类对象的容量操作 三、string类对象的访问及遍历操作 四、string类对象的修改操作 一、string类对象的常见构造 1.string() ——构造空的string类对象&#xff0c;也就是空字符串 2.string(const char* s) ——用字符串来初始化stri…

【Linux】压缩命令——gzip,bzip2,xz

1.压缩文件的用途与技术 你是否有过文件太大&#xff0c;导致无法以正常的E-mail方式发送&#xff1f;又或学校、厂商要求使用CD或DVD来做数据归档之用&#xff0c;但是你的单一文件却都比这些传统的一次性存储媒介还要大&#xff0c;那怎么分成多块来刻录&#xff1f;还有&am…

【QT】显示类控件

显示类控件 显示类控件1. label - 标签2. LCD Number - 显示数字的控件3. ProgressBar - 进度条4. Calendar Widget - 日历5. Line Edit - 输入框6. Text Edit - 多行输入框7. Combo Box - 下拉框8. Spin Box - 微调框9. Date Edit & Time Edit - 日期微调框10. Dial - 旋钮…

Hive 高可用分布式部署详细步骤

目录 系统版本说明 hive安装包下载及解压 上传mysql-connector-java的jar包 配置环境变量 进入conf配置文件中&#xff0c;将文件重命名 在hadoop集群上创建文件夹 创建本地目录 修改hive-site.xml文件 同步到其他的节点服务器 修改node02中的配置 hive-site.xml 修改…

昇思25天学习打卡营第3天|MindSpore张量

# 打卡 目录 # 打卡 类 涉及知识点 1. 创建张量的4种方式 运行例子 2. 张量属性和索引 运行例子 3. 张量运算 运行例子 4. Tensor 与 Numpy 转换 5. 稀疏张量&#xff1a;CSR和COO CSRTensor 运行例子 COOTensor 运行例子 RowTensor 类 import mindspore from…

Linux系统的介绍和常用命令

文章目录 介绍常用命令文件和目录操作文件内容操作系统管理命令网络命令 &#x1f388;个人主页&#xff1a;程序员 小侯 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 ✨收录专栏&#xff1a;Liunx系统 ✨文章内容&#xff1a;Liunx系统介绍 &…

2024年【危险化学品生产单位安全生产管理人员】考试总结及危险化学品生产单位安全生产管理人员考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品生产单位安全生产管理人员考试总结是安全生产模拟考试一点通总题库中生成的一套危险化学品生产单位安全生产管理人员考试试题&#xff0c;安全生产模拟考试一点通上危险化学品生产单位安全生产管理人员作业…

【xinference】(15):在compshare上,使用docker-compose运行xinference和chatgpt-web项目,配置成功!!!

视频演示 【xinference】&#xff08;15&#xff09;&#xff1a;在compshare上&#xff0c;使用docker-compose运行xinference和chatgpt-web项目&#xff0c;配置成功&#xff01;&#xff01;&#xff01; 1&#xff0c;安装docker方法&#xff1a; #!/bin/shdistribution$(…

路径跟踪算法之PID、PP、Stanley详细理解

一、前言 今天又来补作业了&#xff01; 在跟踪控制领域&#xff0c;PID&#xff08;Proportional-Integral-Derivative, 分别为比例、积分、微分&#xff09;、PP&#xff08; Pure-Puresuit, 纯跟踪&#xff09;、Stanley&#xff08;前轮反馈控制&#xff09;是三种最为常见…

STL——map和set

目录 一、set 二、map 1.插入 2.隆重介绍 [] A使用场景 B原理 一、set set即STL库中提供的K模型的二叉搜索树&#xff0c;他的函数使用和其他容器很相似&#xff0c;可以自行阅读文档#include <set> 本文旨对库中难以理解的函数作说明 二、map map即KV模型的二…

【全面讲解如何安装Jupyter Notebook!】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

1.2 如何让机器说人话?万字长文回顾自然语言处理(NLP)的前世今生 —— 《带你自学大语言模型》系列

本系列目录 《带你自学大语言模型》系列部分目录及计划&#xff0c;完整版目录见&#xff1a;带你自学大语言模型系列 —— 前言 第一部分 走进大语言模型&#xff08;科普向&#xff09; 第一章 走进大语言模型 1.1 从图灵机到GPT&#xff0c;人工智能经历了什么&#xff1…

针对tcp不出网打——HTTP隧道代理(以CFS演示)

目录 上传工具到攻击机 使用说明 生成后门文件 由于电脑短路无法拖动文件&#xff0c;我就wget发送到目标主机tunnel.php文件​ 成功上传​ 可以访问上传的文件 启动代理监听 成功带出 后台私信获取弹药库工具reGeorg 上传工具到攻击机 使用说明 生成后门文件 pyt…

HTTP-概述

概念 :Hyper Text Transfer Protocol&#xff0c;超文本传输协议&#xff0c;规定了浏览器和服务器之间数据传输的规则。 原始数据格式 特点 1. 基于TCP协议:面向连接&#xff0c;安全&#xff08;三次握手&#xff09; 2. 基于请求-响应模型的:一次请求对应一次响应&#xf…

移动校园(5):课程表数据获取及展示

首先写下静态页面&#xff0c;起初打算做成一周的课表&#xff0c;由于是以小程序的形式展现&#xff0c;所以显示一周的话会很拥挤&#xff0c;所以放弃下面的方案&#xff0c;改作一次显示一天 改后结果如下&#xff0c;后期还会进行外观优化 真正困难的部分是数据获取 大家大…

2024用友实习笔试

题目一 解题思路&#xff1a; 动态规划&#xff0c;对于当前位置&#xff0c;如果选择&#xff0c;那前一个不能选&#xff0c;前两个及以上不影响。如果不选&#xff0c;那前一个及以上不影响。 题目二 解题思路 数据范围比较小&#xff0c;可以用前缀和然后双重循环遍历每个…

分数的表示和运算方法fractions.Fraction()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 分数的表示和运算方法 fractions.Fraction() 选择题 以下代码三次输出的结果分别是&#xff1f; from fractions import Fraction a Fraction(1, 4) print(【显示】a ,a) b Fraction(1, 2…

解决Visual Studio 一直弹出管理员身份运行问题(win10/11解决办法)

不知道大家是否有遇到这个问题 解决办法也很简单 找到启动文件 如果是快捷方式就继续打开文件位置 找到这个程序启动项 右键 选择 兼容性疑难解答&#xff08;win11 则需要 按住 shift 右键&#xff09; win10 解决办法 这样操作完后就可以了 win11解决办法按以下选择就行