保护模式笔记九 中断门和IDT(中断描述符表)

news2025/2/26 19:56:17

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
段选择子:
在这里插入图片描述

先直观认识一下GDT和段选择子在逻辑地址转换为线性地址中的作用,例如:

给出逻辑地址:21h:12345678h,需要将其转换为线性地址

a. 选择子SEL=21h=0000000000100 0 01b,他代表的意思是:选择子的index=4即100b,选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1(因此有SEL=n<<3,n是索引号)

b. OFFSET=12345678h,若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

保护模式笔记九 中断门和IDT(中断描述符表)

https://www.52pojie.cn/thread-1455684-1-1.html
(出处: 吾爱破解论坛)

前言

所有保护模式索引链接:保护模式笔记一 保护模式介绍

前面学习了调用门之后继续学习中断门

中断门

中断门的作用
先前学习的调用门在实际的Windows中并没有被使用,只是操作系统提供了调用门描述符给开发人员使用。相比之下,Windows使用了中断门,用于:

系统调用(老的CPU通过中断门进入RING(内核)0层;新的CPU使用快速调用)
调试(常见的INT3 对应硬编码为0xCC)

中断门执行流程

根据INT XXX的值 查IDT(中断描述符表),找到对应的段描述符 这个描述符是一个中断门描述符
在中断门描述符中存储另一个代码段的选择子
选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址

IDT

IDT全称Interrupt Descriptor Table(中断描述符表),和GDT相似,IDT也是由一系列描述符组成的。

IDT中存储的段描述符都是系统段描述符
IDT中的第一个元素不是NULL(不为空)
IDT可以包含三种门描述符:①任务门描述符;②中断门描述符;③陷阱门描述符
使用windbg查看IDT的地址和长度:

查看地址:

复制代码 隐藏代码

r idtr

查看长度:

复制代码 隐藏代码

r idtl

在这里插入图片描述

中断门描述符

对比调用门描述符
在这里插入图片描述

中断门描述符结构
在这里插入图片描述

当一个段描述符是一个调用门描述符时,有以下特征:

S位为0,表示该段描述符为系统段描述符(中断门描述符属于系统段描述符)
Type域为1110,表示该段描述符为32位中断门
低16位到31位存储一个段选择子,该段选择子才和代码真正要调用的地址相关
真正要调用的地址 = 段选择子所指向的段.Base + 32位的段中偏移 (段中偏移分为两部分:高位31-16位和低位15-0位)
段.Base默认为0,故真正要调用的地址 = 32位的段中偏移
给出调用门描述符和中断门描述符各部分的对比(上半部分为调用门描述符,下半部分为中断门描述符):
在这里插入图片描述

可以发现中断门描述符和调用门描述符的结构基本一致,只在Type域和参数计数处不同(Type域是描述符的类型标识;中断门不允许传参)

构造中断门描述符
了解了中断门描述符的结构后,尝试自己构造一个无参的中断门描述符,如下:

在这里插入图片描述

得到调用门描述符为:0000EE00`00080000

段中偏移暂时不明确要调用的代码段,先置0

示例代码

接下来给出一段演示代码:

复制代码 隐藏代码

#include <Windows.h>
#include <stdio.h>
int value;
 __declspec(naked) void INTGate(){
         _asm{

                 pushad
                                   pushfd        

                                 mov value,0x610

                                 popfd
                                  popad

                              iretd
         }
 }

int main(){

        //使用 中断门
        _asm{
               int 0x20
        }

                printf("%X\n",value);
        return 0;
}

代码说明

代码十分简单,主要分为两部分:

INTGate:中断门真正要调用的函数,给全局变量赋值,之后中断返回
main:通过中断进入中断门,最后输出全局变量观察是否通过中断门被修改

将门描述符写入IDT

中断索引和IDT地址的对应关系
在代码中,索引的值为0x20,其对应的IDT中的地址为:8003f500

关于索引值和IDT地址的对应关系为:

IDT地址 = 索引值 × 8 + IDT首地址

代入当前的值即为:IDT地址 = 0x20 × 8 + 0x8003f400 = 0x100 + 0x8003f400 = 0x8003f500

确定门描述符
在写入GDT前,还需要确定要写入的值,前面已经构造好了的门描述符为:0x0000EE00`00080000

但其段中偏移还未确定,于是使用VC++ 6.0查看要调用的代码的地址:

进入debug模式,中断后,选中INTGate函数,然后右键→Go to Disassembly(查看反汇编)
在这里插入图片描述
在这里插入图片描述

可以得到要调用的函数的地址为0x00401020

将得到的要调用的函数地址填入门描述符中对应的offset得到:

原:0000EE00`00080000

现:0040EE00`00081020

于是得到确定的门描述符为0040EE00`00081020

确定中断索引并写入门描述符
确定中断索引其实就是确定要写入中断描述符的地址,根据前面中断索引和IDT地址的对应关系,不难倒推出:

中断索引 = (要写入中断描述符的地址 - IDT首地址)÷ 8

因此问题又转换为了确定要写入的中断描述符地址

流程如下图所示:

在这里插入图片描述

用到的指令如下:

1.查看IDT首地址:

复制代码 隐藏代码

r idtr

2.使用指令查看IDT内容:

复制代码 隐藏代码

dq 8003f400 L30

这里的L30代表要查看的长度为 0x30 个qword长度的数据,即0x30个段描述符

3.找到要写入的地址后,将构造好的中断门描述符写入:

复制代码 隐藏代码

eq 8003f500 0040EE00`00081020

同时在确定了要写入的地址后,就可以根据计算出中断索引:

中断索引 = (要写入中断描述符的地址 - IDT首地址)÷ 8 = (8003f500 - 8003f400) ÷ 8 = 0x100 ÷ 8 = 0x20

4.最后再查看写入的地址,确保已正确写入:

dq 8003f500

执行代码
执行结果如下:

在这里插入图片描述

全局变量能够被修改,说明中断门能够正常执行

对比执行前后寄存器和堆栈

执行前寄存器情况
在使用中断门语句处下断点,断下后得到:

在这里插入图片描述

得到此时的寄存器情况:

寄存器 说明 值
在这里插入图片描述

有关段寄存器的详解可回顾:保护模式笔记二 段寄存器

关于标志寄存器的详解可回顾:逆向基础笔记五 标志寄存器

这里简单拆解一下标志寄存器:

先将值转换为二进制得到 0x202→ 0000 0000 0000 0000 0000 0010 0000 0010

按对应的结构填入得到:

在这里插入图片描述

此时IF标志位为1表示当前CPU允许响应INTR可屏蔽中断请求
若IF标志位为0则表示CPU不会响应可屏蔽中断请求
执行前堆栈情况
记录下此时的堆栈情况:

在这里插入图片描述

执行后寄存器情况
为了查看执行后寄存器的情况,在INTGate函数中加入了INT 3引发软中断,但在中断门调用的代码中再引发软中断会引发错误,这里仅作演示观察使用。修改后的INTGate函数如下:

 __declspec(naked) void INTGate(){
         _asm{
                                 int 3                                        //中断
                 pushad
                                   pushfd        

                                 mov value,0x610

                                 popfd
                                  popad

                              iretd
         }
 }

之后INT3中断后查看寄存器情况如下:

在这里插入图片描述

得到此时的寄存器情况:

在这里插入图片描述

执行后堆栈情况
通过内存窗口观察此时的堆栈情况:

在这里插入图片描述

得到此时的堆栈情况:

在这里插入图片描述

对比执行前后寄存器
执行前后寄存器情况如下:
在这里插入图片描述

主要关注到执行前后标志寄存器的变化:

将执行后的EFL按对应的结构拆解得到:

在这里插入图片描述

对比发现,中断门调用后将标志寄存器的IF标志位置为0,表明当前正在处理中断请求,不再响应其它可屏蔽中断

对比执行前后堆栈
执行前后堆栈情况如下:

在这里插入图片描述

不难发现中断门执行后,向堆栈中压入了5个值:SS、ESP、EFL、CS、返回地址

IRETD指令

为了研究IRETD指令干了什么,观察IRETD执行前后堆栈和寄存器的变化情况

IRETD执行前
在这里插入图片描述

通过内存窗口观察执行前的堆栈情况:

在这里插入图片描述

得到此时的堆栈情况:

在这里插入图片描述

再观察此时的寄存器情况:

在这里插入图片描述

在这里插入图片描述

IRETD执行后
通过内存窗口观察执行后的堆栈情况:

在这里插入图片描述

在这里插入图片描述

查看寄存器情况:

在这里插入图片描述

在这里插入图片描述

IRETD执行前后对比
堆栈对比
在这里插入图片描述

寄存器对比
在这里插入图片描述

IRETD返回的时候比RETF多了一个EFL的恢复,关于RETF的内容可回顾:保护模式笔记八 调用门提权(无参+有参)

中断门使用RETF返回

了解了IRETD的原理后,就可以尝试使用RETF来返回

示例代码
示例代码如下:

 __declspec(naked) void INTGate(){
         _asm{
                                   pushad
                                     pushfd        //中断门会修改eflags的IF位为0  所以需要保存标志寄存器

                                    mov eax,[esp+0x24]   //ret
                   mov ebx,[esp+0x28]   //cs

                   //中间少了个esp+0x2c 为EFL

                   mov ecx,[esp+0x30]        //esp
                   mov edx,[esp+0x34]        //ss

                   mov [esp+0x24+4],eax
                   mov [esp+0x28+4],ebx
                   mov [esp+0x2c+4],ecx
                   mov [esp+0x30+4],edx

                                   mov value,0x610

                   popfd
                   popad
                   add esp,4
                   retf
         }
 }

执行结果
在这里插入图片描述

依旧可以正常返回,并且执行正常

代码说明
代码也比较简短简单,可以分为七个部分:

保护现场:pushad、pushfd
将堆栈中的数据取出存到寄存器
将取出来的数据覆盖到堆栈中
全局变量赋值
恢复现场:popfd、popad
堆栈平衡:add esp,4
返回:retf
要理解堆栈数据的覆盖和平衡首先要了解IRETD和RETF的区别

IRETD 中断返回需要堆栈中按顺序存储:返回地址、CS、EFL、ESP、SS 共5个数据

RETF返回需要堆栈中按顺序存储:返回地址、CS、ESP、SS 共4个数据

因此将堆栈中的数据由原本的5个数据替换成4个数据即可

在这里插入图片描述

因此通过对堆栈中数据进行覆盖,即可实现在中断门中使用RETF返回

总结

中断门执行后会将EFL(标志位寄存器)中的IF标志位 置0,使CPU不再响应可屏蔽中断
执行中断门时,分为两种情况:
在没有权限切换时,只向堆栈中压入3个值:①CS;②EFL;③返回地址
在涉及权限切换时,会向堆栈中压入5个值:①SS;②ESP;③EFL;④CS;⑤返回地址
中断门不允许传递参数,调用门允许传递参数
中断门通过INT N(索引)执行,调用门通过远调用 CALL FAR CS:EIP执行
中断门一般使用IRET(16位)/IRETD(32位)返回,调用门一般使用RETF返回
Windows并没有使用调用门,但有使用中断门



【构建操作系统】全局描述符表GDT

https://zhuanlan.zhihu.com/p/25867829
写在前面
添油加醋系列第二弹——剖析GDT

头文件:https://github.com/bajdcc/MiniOS/blob/master/include/gdt.h

实现:https://github.com/bajdcc/MiniOS/blob/master/src/kernel/gdt.c

话说C语言的话除了刷刷OJ外,就是用来实现操作系统这个大头了。C语言比C++少了很多很多臃肿的语法特性,写起来非常优美(至少写操作系统是这样的)。虽说C++有许多的奇技淫巧,一个算法有N种实现方法,但这会让选择恐惧症患者(比如我)难堪,比如说一个类要怎样写啊等等,,抛开其他不谈,假如一个语言的语法特性越少,学起来可能越简单(刚试过lua语法很简单)。OK废话不多说,进入本章主题(涉及OS的资料很杂很偏,如有错误望海涵)。

GDT的构成

这个网址不错(英文的):Global Descriptor Table

首先,根据网上资料,GDT(全局描述符表)又叫段描述符表,暂且就这样认为吧,如有异议可以提出来。

一个GDT可能是这样的(GDT与LDT - Lan’Sir - 博客频道 - CSDN.NET):
在这里插入图片描述

同样也是这样的(Global Descriptor Table):
在这里插入图片描述

在代码中它又是这样:

// 全局描述符表结构 http://www.cnblogs.com/hicjiajia/archive/2012/05/25/2518684.html
// base: 基址(注意,base的byte是分散开的)
// limit: 寻址最大范围 tells the maximum addressable unit
// flags: 标志位 见上面的AC_AC等
// access: 访问权限
struct gdt_entry {
    uint16_t limit_low;       
    uint16_t base_low;
    uint8_t base_middle;
    uint8_t access;
    unsigned limit_high: 4;
    unsigned flags: 4;
    uint8_t base_high;
} __attribute__((packed));

这时你的内心OS:

答案是——它们都是GDT。。
关于C语言的问题:首先,可能有些童鞋不知道struct里那些冒号是神马意思。(C语言 struct结构体的变量声明加冒号)这里叫作“位域”,就是占几个二进制位。同时,它又涉及内存对齐的概念(C语言 结构体的内存对齐问题与位域)。涉及__attribute__((packed))的概念(attribute 你知多少?)它是手动设置对齐大小。

众所周知,一个字节byte是八个bit,那么结构体中有两个4bit的成员,不可能用16bit去容纳它们吧~让它们互相挤挤,节省空间,何乐而不为。

可能看到这里,已经花了好多时间了……没办法,OS的内容非常多,同时GCC的一些怪异偏僻用法又不得不去领会,所以只能一步步来,慢慢理解,急不得。

至于GDT为什么这样描述呢,我自创行不行?一个字——标准,你想改,可能你电脑里的硬件设施不答应……

GDT的存在意义

(GDT 与 LDT - hicjiajia - 博客园)描述得很清楚。

全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。
也就是说,GDT是全局的,存放在内存中的某个位置,而这个位置是由你来指定给CPU的,换句话说,你来钦定!

设置GDT

现在知道了GDT的struct构成(就是一个个数组元素),那么我们要给CPU的就是一个gdt_entry数组地址啦~

那么设置gdt_entry的方法如下:

void gdt_install(uint8_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t flags) {

    /* Setup the descriptor base address */
    gdt[num].base_low = (base & 0xffff);
    gdt[num].base_middle = (base >> 16) & 0xff;
    gdt[num].base_high = (base >> 24) & 0xff;

    /* Setup the descriptor limits */
    gdt[num].limit_low = (limit & 0xffff);
    gdt[num].limit_high = ((limit >> 16) & 0x0f);

    /* Finally, set up the granularity and access flags */
    gdt[num].flags = flags;

    access |= AC_RE; // 设置保留位为1
    gdt[num].access = access;
}

通过实例认识它:

// 宏定义

#define AC_AC 0x1       // 可访问 access
#define AC_RW 0x2       // [代码]可读;[数据]可写 readable for code selector & writeable for data selector
#define AC_DC 0x4       // 方向位 direction
#define AC_EX 0x8       // 可执行 executable, code segment
#define AC_RE 0x10      // 保留位 reserve
#define AC_PR 0x80      // 有效位 persent in memory

// 特权位: 01100000b
#define AC_DPL_KERN 0x0  // RING 0 kernel level
#define AC_DPL_USER 0x60 // RING 3 user level

#define GDT_GR  0x8     // 页面粒度 page granularity, limit in 4k blocks
#define GDT_SZ  0x4     // 大小位 size bt, 32 bit protect mode

// gdt selector 选择子
#define SEL_KCODE   0x1 // 内核代码段
#define SEL_KDATA   0x2 // 内核数据段
#define SEL_UCODE   0x3 // 用户代码段
#define SEL_UDATA   0x4 // 用户数据段
#define SEL_TSS     0x5 // 任务状态段 task state segment http://wiki.osdev.org/TSS

// RPL 请求特权等级 request privilege level
#define RPL_KERN    0x0
#define RPL_USER    0x3

// CPL 当前特权等级 current privilege level
#define CPL_KERN    0x0
#define CPL_USER    0x3

========================================================

/* Setup the GDT pointer and limit */
gp.limit = (sizeof(struct gdt_entry) * NGDT) - 1;
gp.base = (uint32_t)&gdt;

/* null descriptor */
gdt_install(0, 0, 0, 0, 0);  
/* kernel code segment type: code addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_KCODE, 0, 0xfffff, AC_RW|AC_EX|AC_DPL_KERN|AC_PR, GDT_GR|GDT_SZ);
/* kernel data segment type: data addr: 0 limit: 4G gran: 4KB sz: bit 32bit */
gdt_install(SEL_KDATA, 0, 0xfffff, AC_RW|AC_DPL_KERN|AC_PR, GDT_GR|GDT_SZ); 
/* user code segment type: code addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_UCODE, 0, 0xfffff, AC_RW|AC_EX|AC_DPL_USER|AC_PR, GDT_GR|GDT_SZ); 
/* user code segment type: data addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_UDATA, 0, 0xfffff, AC_RW|AC_DPL_USER|AC_PR, GDT_GR|GDT_SZ); 

我的理解是,gdt_install的参数:(段选择子索引号/见题图,基址起始,长度,访问权限,GDT flags)。虽然上述例子中基址起始地址和长度都是一样的(原项目https://github.com/SilverRainZ/OS677是这样写的,可能有点问题),但是访问权限中有AC_EX和AC_DPL_KERN(ring0)/AC_DPL_USER(ring3)的变化,说明每个段的权限是不同的。这些段管理的是同一片内存,只是由于当前索引号的不同,访问/修改内存的权限也不同。

(GDT 与 LDT - hicjiajia - 博客园)讲述了分段管理和分页管理:

分段管理可以把虚拟地址转换成线性地址,而分页管理可以进一步将线性地址转换成物理地址。

(根据段选择子找到)段基指 + 偏移地址 => 线性地址

线性地址 (通过页表) => 物理地址

通过将GDT告诉给CPU后,CPU就知道了操作系统中段的设置,从而可以通过段选择子得到线性地址,在后面实现分页管理后,可进一步将线性地址转换为物理地址(不过当前连物理 址有多大都没法知道呢,在后面会解决)。

段选择子

在这里插入图片描述

(GDT 与 LDT - hicjiajia - 博客园)介绍:

段选择子包括三部分:描述符索引(index)、TI(指示从GDT还是LDT中找)、请求特权级(RPL)。

index部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符gdt_entry。然后用描述符gdt_entry中的段基址SEL加上逻辑地址OFFSET就可以转换成线性地址SEL:OFFSET(看下面给的例子应该就是它们的和SEL+OFFSET)
段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。
请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级),0级最高。关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。

例如:

给出逻辑地址:21h:12345678h,需要将其转换为线性地址

a. 选择子SEL=21h=0000000000100 0 01b,他代表的意思是:选择子的index=4即100b,选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1(因此有SEL=n<<3,n是索引号)

b. OFFSET=12345678h,若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

任务状态段TSS

任务寄存器(TR)用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包含着当前执行任务的重要信息。
TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。

它的初始化和设置:

void tss_init() {
    gdt_install(SEL_TSS, (uint32_t)&tss, sizeof(tss),AC_PR|AC_AC|AC_EX, GDT_GR); 
    /* for tss, access_reverse bit is 1 */
    gdt[5].access &= ~AC_RE;
}

// 装载TSS
void tss_install() {
    __asm__ volatile("ltr %%ax" : : "a"((SEL_TSS << 3)));
}

// 设置TSS
void tss_set(uint16_t ss0, uint32_t esp0) {
    // 清空TSS
    memset((void *)&tss, 0, sizeof(tss));
    tss.ss0 = ss0;
    tss.esp0 = esp0;
    tss.iopb_off = sizeof(tss);
}

跟GDT也差不了多少,只是GDT_SZ没有了,也指定了tss的地址,并设置gdt_entry的保留位为1(至于为啥我没有仔细查)。至于__asm__ volatile的GCC在C语言中内嵌汇编 asm __volatile__我也没全部搞明白怎么用。SEL_TSS << 3的话要参考选择子的构成,它高13位是索引,所以要乘8。

关于ltr指令(设置TSS结构中堆栈信息的 ltr 指令):

在任务内发生特权级变换时堆栈也随着自动切换,外层堆栈指针保存在内层堆栈中,而内层堆栈指针存放在当前任务的TSS中。所以,在从外层向内层变换时,要访问TSS(从内层向外层转移时不需要访问TSS,而只需访问内层栈中保存的栈指针)。
LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择子。LTR指令从GDT中取出相应的TSS段描述符,把TSS段描述符的基地址和界限等信息装入TR的高速缓冲寄存器中。
TSS的构成在https://github.com/bajdcc/MiniOS/blob/master/include/idt.h中(看下面的英文注释/Task State Segment,就是说SS0、ESP0比较重要)。

// 任务状态段 task state segment http://wiki.osdev.org/TSS
// The only interesting fields are SS0 and ESP0.
// SS0 gets the kernel datasegment descriptor (e.g. 0x10 if the third entry in your GDT describes your kernel’s data)
// ESP0 gets the value the stack-pointer shall get at a system call
// IOPB may get the value sizeof(TSS) (which is 104) if you don’t plan to use this io-bitmap further (according to mystran in http://forum.osdev.org/viewtopic.php?t=13678)

// http://blog.csdn.net/huybin_wang/article/details/2161886
// TSS的使用是为了解决调用门中特权级变换时堆栈发生的变化

// http://www.kancloud.cn/wizardforcel/intel-80386-ref-manual/123838
/*
TSS 状态段由两部分组成:
1、 动态部分(处理器在每次任务切换时会设置这些字段值)
通用寄存器(EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI)
段寄存器(ES,CS,SS,DS,FS,GS)
状态寄存器(EFLAGS)
指令指针(EIP)
前一个执行的任务的TSS段的选择子(只有当要返回时才更新)
2、 静态字段(处理器读取,但从不更改)
任务的LDT选择子
页目录基址寄存器(PDBR)(当启用分页时,只读)
内层堆栈指针,特权级0-2
T-位,指示了处理器在任务切换时是否引发一个调试异常
I/O 位图基址
*/

struct tss_entry {
    uint32_t link;
    uint32_t esp0;
    uint32_t ss0;
    uint32_t esp1;
    uint32_t ss1;
    uint32_t esp2;
    uint32_t ss2;
    uint32_t cr3;
    uint32_t eip;
    uint32_t eflags;
    uint32_t eax;
    uint32_t ecx;
    uint32_t edx;
    uint32_t ebx;
    uint32_t esp;
    uint32_t ebp;
    uint32_t esi;
    uint32_t edi;
    uint32_t es;
    uint32_t cs;
    uint32_t ss;
    uint32_t ds;
    uint32_t fs;
    uint32_t gs;
    uint32_t ldtr;
    uint16_t padding1;
    uint16_t iopb_off;
} __attribute__ ((packed));

阶段性总结

涉及OS的内容真是庞大,单单一个GDT就涉及巨量的知识,包括结构体定义、汇编指令、GCC黑魔法、参数的使用等,还涉及了TSS,目标仅仅是实现分段管理。而后面还有中断管理、物理内存管理、虚拟内存管理等一系列内容,篇幅绝对不比本文少,真令人望洋兴叹。

原始项目OS67中也存在着一些错误,有些错误像是单词拼写等我已经纠正了,还有些如软盘访问我去参考了网上的资料,与OS67的不一致,但我没采用OS67的。毕竟OS67也是其作者自己摸索出来的,让我跳过了许多坑。。不过我想后面的进程管理还是得自己写才能体会更深。

既然OS的内容很杂很多,所以也只能挑一些重点的讲讲了,不可能面面俱到,在后面的编写/借鉴中,还是要以查资料为主,给源码附上参考文章的地址,方便阅读。



Linux中断一网打尽(2) - IDT及中断处理的实现

https://zhuanlan.zhihu.com/p/106318141

通过阅读本文您可以了解到:

  • IDT是什么 ;
  • IDT如何被初始化;
  • 什么是门;
  • 传统系统调用是如何实现的;
  • 硬件中断的实现;
    如何设置IDT
    IDT 中断描述符表定义
    中断描述符表简单来说说是定义了发生中断/异常时,CPU按这张表中定义的行为来处理对应的中断/异常。
#define IDT_ENTRIES                     256
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;

从上面我们可以知道,其包含了256项,它是一个gate_desc的数据,其下标0-256就表示中断向量,gate_desc我们在下面马上介绍。

中断描述符项定义

当中断发生,cpu获取到中断向量后,查找IDT中断描述符表得到相应的中断描述符,再根据中断描述符记录的信息来作权限判断,运行级别转换,最终调用相应的中断处理程序;
这里涉及到Linux kernel的分段式内存管理,我们这里不详细展开,有兴趣的同学可以自行学习。如下简述之:
我们知道CPU只认识逻辑地址,逻辑地址经分段处理转换成线性地址,线性地址经分页处理最终转换成物理地址,这样就可以从内存中读取了;
逻辑地址你可以简单认为就是CPU执行代码时从CS(代码段寄存器) : IP (指令计数寄存器)中加载的代码,实际上通过CS可以得到逻辑地址的基地址,再加上IP这个相对于基地址的偏移量,就得到真正的逻辑地址;
CS寄存器16位,它不会包含真正的基地址,它一般被称为段选择子,包括一个index索引,指向GDT或 LDT的一项;一个指示位,指示index索引是属于GDT还是LDT; 还有CPL, 表明当前代码运行权限;
GDT: 全局描述符表,每一项记录着相应的段基址,段大小,段的访问权限DPL等,到这里终于可以获取到段基地址了,再加上之前IP寄存器里存放的偏移量,真正的逻辑地址就有了。
附上简图:
在这里插入图片描述

我们先看中断描述符的定义:

struct gate_struct {
        u16             offset_low;
        u16             segment;
        struct idt_bits bits;
        u16             offset_middle;
  #ifdef CONFIG_X86_64
        u32             offset_high;
        u32             reserved;
  #endif
  } __attribute__((packed));

其中:

offset_high,offset_middle和offset_low合起来就是中断处理函数地址的偏移量;
segment就是相应的段选择子,根据它在GDT中查找可以最终获取到段基地址;
bits是该中断描述符的一些属性值:

struct idt_bits {
        u16             ist     : 3,
                        zero    : 5,
                        type    : 5,
                        dpl     : 2,
                        p       : 1;
} __attribute__((packed));

ist表示此中断处理函数是使用pre-cpu的中断栈,还是使用IST的中断栈;

type表示所中断是何种类型,目前有以下四种:

enum {
        GATE_INTERRUPT = 0xE, //中断门
        GATE_TRAP = 0xF, // 陷入门
        GATE_CALL = 0xC, // 调用门
        GATE_TASK = 0x5, // 任务门
     };

门的概念这里主要用作权限控制,我们从一个区域进到另一个区域需要通过一扇门,有门禁权限才可以通过,因此 dpl就是这个权限,实际中我们一般称为RPL;

我们后面会通过一个例子来讲一下CPL,RPL和DPL三者之间的关系。

IDT 中断描述符表本身的存储

IDT 中断描述符表的物理地址存储在IDTR寄存器中,这个寄存器存储了IDT的基地址和长度。查询时,从 IDTR 拿到 base address ,加上向量号 * IDT entry size,即可以定位到对应的表项(gate)。
在这里插入图片描述

设置IDT

设置中断门类型的IDT描述符

static void set_intr_gate(unsigned int n, const void *addr)
  {
        struct idt_data data;

        BUG_ON(n > 0xFF);

        memset(&data, 0, sizeof(data));
        data.vector     = n; // 中断向量
        data.addr       = addr; // 中断处理函数的地址
        data.segment    = __KERNEL_CS; // 段选择子
        data.bits.type  = GATE_INTERRUPT; // 类型
        data.bits.p     = 1;

        idt_setup_from_table(idt_table, &data, 1, false);
  }

上面的函数主要是填充好idt_data,然后调用idt_setup_from_table;

idt_setup_from_table:
static void
  idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
  {
        gate_desc desc;

        for (; size > 0; t++, size--) {
                idt_init_desc(&desc, t);
                write_idt_entry(idt, t->vector, &desc);
                if (sys)
                        set_bit(t->vector, system_vectors);
        }
  }

首先使用 idt_data结构来填充中断描述符变量idt_init_desc, 然后将这个中断描述符变量copy进idt_table。

看,就是这么简单~~~

gate_desc的多种初始化方法
因为gate_desc是通过ida_dat填充的,所以这里关键是idt_data的初始化,我们详细看一下:

/* Interrupt gate 
  中断门,DPL = 0
  只能从内核调用
  */
  #define INTG(_vector, _addr)                          \
        G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)

  /* System interrupt gate
  系统中断门,DPL = 3
  可以从用户态调用,比如系统调用
  */
  #define SYSG(_vector, _addr)                          \
        G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

  /*
   * Interrupt gate with interrupt stack. The _ist index is the index in
   * the tss.ist[] array, but for the descriptor it needs to start at 1.
   中断门, DPL = 0
   只能从内核态调用,使用TSS.IST[]作为中断栈 
   */
  #define ISTG(_vector, _addr, _ist)                    \
        G(_vector, _addr, _ist + 1, GATE_INTERRUPT, DPL0, __KERNEL_CS)

  /* Task gate
  任务门, DPL = 0
  只能作内核态调用 
  */
  #define TSKG(_vector, _gdt)                           \
        G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)

我们再来看下G这个宏的实现:

#define G(_vector, _addr, _ist, _type, _dpl, _segment)        \
        {                                               \
                .vector         = _vector,              \
                .bits.ist       = _ist,                 \
                .bits.type      = _type,                \
                .bits.dpl       = _dpl,                 \
                .bits.p         = 1,                    \
                .addr           = _addr,                \
                .segment        = _segment,             \
        }

实际上就是填充idt_data的各个字段。

传统系统调用的实现
这里所说的传统系统调用主要指旧的32位系统使用 int 0x80软件中断来进入内核态,实现的系统调用。因为这种传统系统调用方式需要进入内核后作权限验证,还要切换内核栈后作大量压栈方式,调用结束后清理栈作恢复,两个字太慢,后来CPU从硬件上支持快速系统调用sysenter/sysexit, 再后来又发展到syscall/sysret, 这两种都不需要通过中断方式进入内核态,而是直接转换到内核态,速度快了很多。

传统系统调用相关 IDT 的设置
Linux系统启动过程中内核压解后最终都调用到start_kernel, 在这里会调用trap_init, 然后又会调用idt_setup_traps:

void __init idt_setup_traps(void)
  {
        idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
  }
我们来看这里的def_idts的定义:

static const __initconst struct idt_data def_idts[] = {
        ....
  #if defined(CONFIG_IA32_EMULATION)
        SYSG(IA32_SYSCALL_VECTOR,       entry_INT80_compat),
  #elif defined(CONFIG_X86_32)
        SYSG(IA32_SYSCALL_VECTOR,       entry_INT80_32),
  #endif
  };

​ 上面的SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)就是设置系统调用的异常中断处理程序,其中 #define IA32_SYSCALL_VECTOR 0x80

再看一下SYSG的定义:

#define SYSG(_vector, _addr)                            \
        G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

它初始化一个中断门,权限是DPL3, 因此从用户态是允许发起系统调用的。

我们调用系统调用,不大可能自已手写汇编代码,都是通过glibc来调用,基本流程是保存参数到寄存器,然后保存系统调用向量号到eax寄存器,然后调用int 0x80进入内核态,切换到内核栈,将用户态时的ss/sp/eflags/cs/ip/error code依次压入内核栈。
entry_INT80_32系统调用对应的中断处理程序

assembly ENTRY(entry_INT80_32) ASM_CLAC pushl %eax / pt_regs->orig_ax /

SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1    /* save rest */

    TRACE_IRQS_OFF

    movl    %esp, %eax
    call    do_int80_syscall_32
.Lsyscall_32_done: ... .Lirq_return:

INTERRUPT_RETURN
... ENDPROC(entry_INT80_32) ```

我们略去了中间的一些细节部分,可以看到首先将中断向量号压栈,再保存所有当前的寄存器值到pt_regs, 保存当前栈指针到%eax寄存器,最后再调用 do_int80_syscall_32, 这个函数中就会执行具体的中断处理,然后INTERRUPT_RETURN恢复栈,作好返回用户态的准备。

do_int80_syscall_32调用 do_syscall_32_irqs_on,我们看一下其实现:

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
  {
        struct thread_info *ti = current_thread_info();
        unsigned int nr = (unsigned int)regs->orig_ax;

  #ifdef CONFIG_IA32_EMULATION
        ti->status |= TS_COMPAT;
  #endif

        if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {
                nr = syscall_trace_enter(regs);
        }

        if (likely(nr < IA32_NR_syscalls)) {
                nr = array_index_nospec(nr, IA32_NR_syscalls);
  #ifdef CONFIG_IA32_EMULATION
                regs->ax = ia32_sys_call_table[nr](regs);
  #else
                regs->ax = ia32_sys_call_table[nr](
                        (unsigned int)regs->bx, (unsigned int)regs->cx,
                        (unsigned int)regs->dx, (unsigned int)regs->si,
                        (unsigned int)regs->di, (unsigned int)regs->bp);
  #endif /* CONFIG_IA32_EMULATION */
        }

        syscall_return_slowpath(regs);
  }

通过中断向量号nr从ia32_sys_call_table中断向量表中索引到具体的中断处理函数然后调用之,其结果最终合存入%eax寄存器。

一图以蔽之

在这里插入图片描述

硬件中断的实现

硬件中断的IDT初始化和调用流程
这里我们不讲解具体的代码细节,只关注流程 。

硬件中断相关IDT的初始化也是在Linux启动时完成,在start_kernel中通过调用init_IRQ完成,我们来看一下:

void __init init_IRQ(void)
{
int i;
for (i = 0; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);

    BUG_ON(irq_init_percpu_irqstack(smp_processor_id()));

    x86_init.irqs.intr_init(); // 即调用  native_init_IRQ

}

void __init native_init_IRQ(void)
{
        /* Execute any quirks before the call gates are initialised: */
        x86_init.irqs.pre_vector_init();

        idt_setup_apic_and_irq_gates();
        lapic_assign_system_vectors();

        if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
                setup_irq(2, &irq2);
}


重点在于idt_setup_apic_and_irq_gates:

```c
*/
void __init idt_setup_apic_and_irq_gates(void)
{
        int i = FIRST_EXTERNAL_VECTOR;
        void *entry;

        idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);

        for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {
                entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);
                set_intr_gate(i, entry);
        }
}

其中的set_intr_gate用来初始化硬件相关的调用门,其对应的中断门处理函数在irq_entries_start中定义,它位于arch/x86/entry/entry_64.S中:

.align 8
ENTRY(irq_entries_start)
    vector=FIRST_EXTERNAL_VECTOR
    .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
        UNWIND_HINT_IRET_REGS
        pushq   $(~vector+0x80)                 /* Note: always in signed byte range */
        jmp     common_interrupt
        .align  8
        vector=vector+1
    .endr
END(irq_entries_start)

这段汇编实现对不大熟悉汇编的同学可能看起来有点晕,其实很简单它相当于填充一个中断处理函数的数组,填充多少次呢? (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)这就是次数,数组的每一项都是一个函数:

UNWIND_HINT_IRET_REGS
        pushq   $(~vector+0x80)                 /* Note: always in signed byte range */
        jmp     common_interrupt

即先将中断号压栈,然后跳转到common_interrupt执行,可以看到这个common_interrupt是硬件中断的通用处理函数,它里面最主要的就是调用do_IRQ:

__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
        struct pt_regs *old_regs = set_irq_regs(regs);
        struct irq_desc * desc;
        /* high bit used in ret_from_ code  */
        unsigned vector = ~regs->orig_ax;

        entering_irq();

        /* entering_irq() tells RCU that we're not quiescent.  Check it. */
        RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");

        desc = __this_cpu_read(vector_irq[vector]);
        if (likely(!IS_ERR_OR_NULL(desc))) {
                if (IS_ENABLED(CONFIG_X86_32))
                        handle_irq(desc, regs);
                else
                        generic_handle_irq_desc(desc);
        } else {
                ack_APIC_irq();

                if (desc == VECTOR_UNUSED) {
                        pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
                                             __func__, smp_processor_id(),
                                             vector);
                } else {
                        __this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
                }
        }

        exiting_irq();

        set_irq_regs(old_regs);
        return 1;
}

首先根据中断向量号获取到对应的中断描述符irq_desc, 然后调用generic_handle_irq来处理:

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
        desc->handle_irq(desc);
}

这里最终会调用到中断描述符的handle_irq,因此另一个重点就是这个中断描述符的设置了,它可以单开一篇文章来讲,我们暂不详述了。

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

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

相关文章

H3C 路由过滤路由引入实验

H3C 路由过滤&路由引入实验 实验拓扑 ​​ 实验需求 按照图示配置 IP 地址&#xff0c;R1&#xff0c;R3&#xff0c;R4 上使用 loopback 口模拟业务网段R1 和 R2 运行 RIPv2&#xff0c;R2&#xff0c;R3 和 R4 运行 OSPF&#xff0c;各自协议内部互通在 RIP 和 OSPF …

OpenAI划时代大模型——文本生成视频模型Sora作品欣赏(十五)

Sora介绍 Sora是一个能以文本描述生成视频的人工智能模型&#xff0c;由美国人工智能研究机构OpenAI开发。 Sora这一名称源于日文“空”&#xff08;そら sora&#xff09;&#xff0c;即天空之意&#xff0c;以示其无限的创造潜力。其背后的技术是在OpenAI的文本到图像生成模…

Gemma模型一些细节讲解

Gemma模型报告中提到的几个点进行代码细节解读一下&#xff1a; &#xff08;1&#xff09;Embedding层共享参数 &#xff08;2&#xff09;输入输出层均进行RMSNorm Embedding层共享参数 共享embedding的权重给最后的llm_head层。是词嵌入层的共享&#xff0c;与旋转位置编码…

低密度奇偶校验码LDPC(七)——SPA和积译码算法的简化

往期博文 低密度奇偶校验码LDPC&#xff08;一&#xff09;——概述_什么是gallager构造-CSDN博客 低密度奇偶校验码LDPC&#xff08;二&#xff09;——LDPC编码方法-CSDN博客 低密度奇偶校验码LDPC&#xff08;三&#xff09;——QC-LDPC码概述-CSDN博客 低密度奇偶校验码…

CNAN知识图谱辅助推荐系统

CNAN知识图谱辅助推荐系统 文章介绍了一个基于KG的推荐系统模型&#xff0c;代码也已开源&#xff0c;可以看出主要follow了KGNN-LS 。算法流程大致如下&#xff1a; 1. 算法介绍 算法除去attention机制外&#xff0c;主要的思想在于&#xff1a;user由交互过的item来表示、i…

前端小案例——登录界面(正则验证, 附源码)

一、前言 实现功能&#xff1a; 提供用户名和密码输入框。当用户提交表单时&#xff0c;阻止默认提交行为。使用正则表达式验证用户输入的内容&#xff0c;判断输入的是有效的邮箱地址还是身份证号码。根据验证结果&#xff0c;在输入框下方显示相应的提示信息。 实现逻辑&a…

【C++庖丁解牛】初始化列表 | Static对象 | 友元函数

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1. 再谈构造函数1.1 …

mininet虚拟网络中的主机与宿主Ubuntu及因特网互通实现

环境: Win10(物理机),Vmware workstation ,Ubuntu(vm中的虚拟机),mininet 构建的虚拟网络环境说明: 在一win10的物理机中安装了vm平台,在vm中加载了一ubuntun系统,在改ubuntu系统中安装了mininet。 目标: 通过mininet 构建虚拟网络环境(使用python代码构建一个交换…

【开源】SpringBoot框架开发用户画像活动推荐系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 兴趣标签模块2.3 活动档案模块2.4 活动报名模块2.5 活动留言模块 三、系统设计3.1 用例设计3.2 业务流程设计3.3 数据流程设计3.4 E-R图设计 四、系统展示五、核心代码5.1 查询兴趣标签5.2 查询活动推荐…

mysql 同一条排序语句查询出来的结果某几条没按照排序查询

1501这个机床order 1 2 3 1 2 3 1 2 3 1502这个机床order 2 3 1 2 3 1 2 3 1 原因是order存在一致的 第一个123 第二个也有123 所以存在随机情况 正常应该是123456 但是需求是123 123 所以再按照id重新排序一下 原sql :select bindType.id, bindType.process_num as…

大模型学习笔记五:RAG

文章目录 一、RAG介绍1)局限性2)通过检索增强生成二、RAG系统的基本搭建流程1)搭建流程简介2)文档的加载和切割3)检索引擎4)LLM接口封装5)prompt模板6)RAG Pipeline初探7)关键字检索局限性三、向量检索1)文本向量2)向量相似度计算3)向量数据库4)基于向量检索的RAG…

一本书讲透ChatGPT,实现从理论到实践的跨越!大模型技术工程师必读书籍【送书活动】

目录 前言一、内容简介二、作者简介三、专家推荐四、读者对象五、目录福利总结 前言 OpenAI 在 2022 年 11 月推出了人工智能聊天应用—ChatGPT。它具有广泛的应用场景&#xff0c;在多项专业和学术基准测试中表现出的智力水平&#xff0c;不仅接近甚至有时超越了人类的平均水平…

40个Python字符串实例

Python 字符串是 Python 编程语言中最常用的数据类型之一&#xff0c;它可以表示文本或一组字符。Python 中的字符串是不可变的序列&#xff0c;意味着一旦创建&#xff0c;其值就不能被修改。下面是一些关于 Python 字符串的介绍。 概述 创建字符串&#xff1a;可以使用单引…

HI3516DV300 HI3516DRBCV300 海思安防监控芯片

Hi3516D V300是专为行业专用智能高清网络摄像机设计的新一代SoC。引入新一代ISP、最新H.265视频压缩编码器、高性能NNIE引擎&#xff0c;使Hi3516D V300在低码率、高图像质量、智能处理分析、低功耗等方面领先业界。能量消耗。Hi3516D V300集成了POR、RTC、音频编解码器和待机唤…

AI大模型的预训练、迁移和中间件编程

大家好&#xff0c;我是爱编程的喵喵。双985硕士毕业&#xff0c;现担任全栈工程师一职&#xff0c;热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…

测试用例术语5.0

一、软件测试中术语 1.动态测试&#xff08;dynamic testing&#xff09;&#xff1a;通过运行软件的组件或 系统来测试软件 例如&#xff1a;一辆汽车发动并行使测试 2.静态测试&#xff08;static testing&#xff09;&#xff1a;对组件的规格说明书进行 评审&#xff0c…

Linux——自写一个简易的shell

目录 前言 一、打印提示信息 二、分割字符串 三、替换程序 前言 之前学习了很多进程相关的知识&#xff0c;包括环境变量、进程的创建与退出、进程等待、进程替换。现在可以用所学的作一个小总结&#xff0c;手撕一个shell解释器&#xff0c;大致的思路是先通过环境变量获…

【轮式平衡机器人】——TMS320F28069片内外设之Timer_IT(补:CCS程序烧录方法)

引入 Timer_IT 指的是 TMS320F28069 的定时器中断功能。在微控制器或数字信号控制器中&#xff0c;定时器是一个非常重要的外设&#xff0c;它可以用来产生固定时间间隔的中断&#xff0c;或者用来精确计算时间。 Timer_IT 的主要特点如下&#xff1a; 定时功能&#xff1a;…

数组常见算法

一、数组排序 冒泡排序 本篇我们介绍最基本的排序方法&#xff1a;冒泡排序。 实现步骤 1、比较两个相邻元素&#xff0c;如果第一个比第二个大&#xff0c;就交换位置 2、对每一对相邻元素进行同样的操作&#xff0c;除了最后一个元素 特点 每一轮循环后都会把最大的一个…

Java基础概念 1-6注释关键字字面量变量-基本用法变量-使用方式和注意事项变量练习-计算公交车的人数

Java基础概念 1-注释 单行注释 // 多行注释 /* */ 文档注释 /** */ --暂时不用 例: public class HelloWorld{ //main方法,表示程序的主入口.public static void main (String[] args){/*输出语句(打印语句)会把小括号内的内容进行输出打印.*/System.out.…