kernel/interrupt.c
补充了获取中断状态和设置中断的函数
#include "io.h"
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "print.h"
#define IDT_DESC_CNT 0x21 //支持的中断数目
#define PIC_M_CTRL 0x20 // 主片的控制端口是 0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是 0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是 0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是 0xa1
#define EFLAGS_IF 0x00000200 //eflags 寄存器if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0":"=g"(EFLAG_VAR));
// 中断门描述符结构体
struct gate_desc{
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount;
uint8_t attribute;
uint16_t func_offset_high_word;
};
//静态函数声明 非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; // idt 是中断描述符表
// 声明引用定义在 kernel.S 中的中断处理函数入口数组
extern intr_handler intr_entry_table[IDT_DESC_CNT];
char *intr_name[IDT_DESC_CNT]; //保存异常的名字
/*定义中断处理程序数组,在kernel.asm中定义的intrXXentry
只是中断处理程序的入口,最终调用的是 ide_table 中的处理程序*/
intr_handler idt_table[IDT_DESC_CNT];
/*初始化可编程中断处理器 8259A*/
static void pic_init(void) {
/*初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联 8259, 需要 ICW4
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为 0x20
// 也就是 IR[0-7] 为 0x20 ~ 0x27
outb (PIC_M_DATA, 0x04); // ICW3: IR2 接从片
outb (PIC_M_DATA, 0x01); // ICW4: 8086 模式, 正常 EOI
/*初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联 8259, 需要 ICW4
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为 0x28
// 也就是 IR[8-15]为 0x28 ~ 0x2F
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的 IR2 引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086 模式, 正常 EOI
/*打开主片上 IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe); //1111 1110 fe
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (int32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void){
int i;
for(i = 0; i < IDT_DESC_CNT; ++i){
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*通用的中断处理函数*/
static void general_intr_handler(uint8_t vec_nr){
if(vec_nr == 0x27 || vec_nr == 0x2f){
//IRQ7和IRQ5会产生伪中断,无需处理
//0x2f是从片 8259A 上的最后一个 IRQ 引脚,保留项
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_char(' ');
put_str(intr_name[vec_nr]);
put_char('\n');
}
/*完成一般中断处理函数注册及异常名称注册*/
static void exception_init(void){
int i;
for(i = 0; i < IDT_DESC_CNT; ++i){
/*idt_table中的函数是在进入中断后根据中断向量好调用的
见kernel/kernel.asm 的 call[idt_table + %1*4]*/
idt_table[i] = general_intr_handler;
//默认为general_intr_handler
//以后会有 register_handler注册具体的处理函数
intr_name[i] = "unknown"; //先统一为 "unknown"
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第 15 项是 intel 保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
intr_name[0x20] = "#CLOCK";
}
/*获取当前中断状态*/
enum intr_status intr_get_status(void){
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
/*设置中断状态*/
enum intr_status intr_set_status(enum intr_status status){
return (status & INTR_ON) ? intr_enable():intr_disable();
}
/*开中断,返回之前的状态*/
enum intr_status intr_enable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
}else{
old_status = INTR_OFF;
asm volatile("sti");//关中断 cli 将IF置1
}
return old_status;
}
/*关闭中断*/
enum intr_status intr_disable(void){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
asm volatile("cli" : : : "memory");
}else{
old_status = INTR_OFF;
}
return old_status;
}
/*完成有关中断的所有初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
exception_init(); //异常名和中断处理函数初始化
pic_init(); //初始化 8259A
/*加载 idt*/
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)((uint32_t)idt << 16)));
asm volatile("lidt %0"::"m"(idt_operand));
put_str("idt_init done\n");
}
kernel / interrupt.h
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
enum intr_status{
INTR_OFF,
INTR_ON
};
void idt_init();
enum intr_status intr_get_status(void);
enum intr_status intr_set_status(enum intr_status);
enum intr_status intr_enable(void);
enum intr_status intr_disable(void);
#endif
kernel / debug.h
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename, int line, const char* func, const char* condition);
/************* __VA_ARGV__ *********
* __VA_ARGS__ 是预处理器所支持的专用标识符
* 代表所有与省略号相对应的参数
* "..."表示定义的宏其参数可变。
*/
#define PANIC(...) panic_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)
/*********************************************/
#ifdef NDEBUG
#define ASSERT(CONDITION) ((void)0)
#else
#define ASSERT(CONDITION) \
if(CONDITION){}else{ \
/*符号#让编译器将宏的参数转化为字符串字面量 */ \
PANIC(#CONDITION); \
}
#endif /*__NDEBUG*/
#endif /*__KERNEL_DEBUG_H*/
kernel / debug.c
#include "debug.h"
#include "print.h"
#include "interrupt.h"
/*打印文件名、行号、函数名、条件并使程序悬停*/
void panic_spin(char *filename, int line, const char *func, const char *cons )
{
intr_disable();//有时候会单独调用 panic_spin , 所以在此处关中断
put_str("\n\n\n!!!!! error !!!!!\n");
put_str("filename:");put_str(filename);put_str("\n");
put_str("line:0x");put_int(line);put_str("\n");
put_str("function:");put_str((char*)func);put_str("\n");
put_str("condition:");put_str((char*)cons);put_str("\n");
while(1);
}
kernel / main.c
#include "print.h"
#include "init.h"
#include "debug.h"
int main(void){
put_str("I am kernel\n");
init_all();
ASSERT(1==2);
//asm volatile("sti");
while(1);
return 0;
}
运行结果:
lib / string.h
#ifndef __LIB_STRING_H
#define __LIB_STRING_H
#include "stdint.h"
/*将dst_起始的size个字节置为value*/
void memset(void *dst_, uint8_t value, uint32_t size);
/*将src_起始的size个字节复制到 dst_ */
void memcpy(void *dst_, const void *src, uint32_t size);
/*连续比较以地址a_和地址b_开头的size个字节.
相等则返回 0, a>b return 1, a
int memcmp(const void *a_, const void *b_, uint32_t size);
/*src_ 复制到 dst_*/
char *strcpy(char *dst_, const char *src_);
/*返回字符串长度*/
uint32_t strlen(const char *str){;
/*比较两个字符串,若a_中的字符大于b_中的字符调用1,相等返回0,否则返回-1*/
int8_t strcmp(const char *a, const char *b);
/*左到右找str中首次出现的 ch 的地址*/
char *strchr(const char *str, const uint8_t ch);
/*从后往前找字符串str中首次出现的ch的地址*/
char *strrchr(const char *str, const uint8_t ch);
/*将字符串 src 拼接到dst_后,返回拼接的字符串地址*/
char *strcat(char *dst_, const char *str_);
/*在字符串str中查找字符ch出现的次数*/
uint32_t strchrs(const char *str, uint8_t ch);
#endif /*__LIB_STRING_H*/
lib / string.c
#include "string.h"
#include "global.h"
#include "debug.h"
/*将dst_起始的size个字节置为value*/
void memset(void *dst_, uint8_t value, uint32_t size)
{
ASSERT(dst_ != NULL);
uint8_t *dst = (uint8_t*)dst_;
while(size-- > 0){
*dst++ = value;
}
return;
}
/*将src_起始的size个字节复制到 dst_ */
void memcpy(void *dst_, const void *src, uint32_t size){
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t *dst = dst_;
const uint8_t *src = src_;
while(size-- > 0){
*dst++ = *src++;
}
}
/*连续比较以地址a_和地址b_开头的size个字节.
相等则返回 0, a>b return 1, a
int memcmp(const void *a_, const void *b_, uint32_t size)
{
const char *a = a_;
const char *b = b_;
ASSERT(a != NULL || b != NULL);
while(size-- > 0){
if(*a != *b){
return (*a > *b) ? 1: -1;
}
++a;
++b;
}
return 0;
}
/*src_ 复制到 dst_*/
char *strcpy(char *dst_, const char *src_)
{
ASSERT(dst_ != NULL && src_ != NULL);
char *r = dst_;
while((*dst_ ++ = *src_ ++));
return r;
}
/*返回字符串长度*/
uint32_t strlen(const char *str){
ASSERT(str != NULL);
const char *p = str;
while(*p++);
return (p - str - 1);
}
/*比较两个字符串,若a_中的字符大于b_中的字符调用1,相等返回0,否则返回-1*/
int8_t strcmp(const char *a, const char *b)
{
ASSERT(a != NULL && b != NULL);
while(*a != 0 && *a == *b){
++a;
++b;
}
//if(*a < *b) return -1;
//if(*a > *b) return 1; else return 0;
return (*a < *b) ? -1 : (*a > *b);
}
/*左到右找str中首次出现的 ch 的地址*/
char *strchr(const char *str, const uint8_t ch)
{
ASSERT(str != NULL);
while(*str != 0){
if(*str == ch){
return (char *)str;//需要强制转化为返回值类型。
}
++str;
}
return NULL;
}
/*从后往前找字符串str中首次出现的ch的地址*/
char *strrchr(const char *str, const uint8_t ch)
{
ASSERT(str != NULL);
const char *last_char = NULL;
while(*str != 0){
if(*str == ch){
last_char = str;
}
++str;
}
return (char *)last_char;
}
/*将字符串 src 拼接到dst_后,返回拼接的字符串地址*/
char *strcat(char *dst_, const char *str_)
{
ASSERT(dst_ != NULL && src_ != NULL);
char *str = dst_;
while(*str++);
--str;
while((*str++ = *src_++));
return dst_;
}
/*在字符串str中查找字符ch出现的次数*/
uint32_t strchrs(const char *str, uint8_t ch){
ASSERT(str != NULL);
uint32_t ch_cnt = 0;
const char *p = str;
while(*p != 0){
if(*p == ch){
++ch_cnt;
}
++p;
}
return ch_cnt;
}
一个字节有8位,所以位图的一个字节对应8个资源单位。假设是管理内存,每一位都将表示实际物理内存中的 4KB。也就是 1页。如果某位为0,就是可以分配,如果某位为1,就是不可分配。
lib / kernel / bitmap.h
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1
struct bitmap{
uint32_t btmp_bytes_len;
/*遍历位图时候是以字节为单位,微操是位。
所以此处的位图指针必须是单字节*/
uint8_t *bits;
};
void bitmap_init(struct bitmap *btmp);
bool bitmap_scan_test(struct bitmap *btmp, uint32_t bit_idx);
int bitmap_scan(struct bitmap *btmp, uint32_t cnt);
void bitmap_set(struct bitmap *btmp, uint32_t bit_idx, int8_t value);
lib/ kernel/ bitmap.c
#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"
/*初始化 btmp 位图*/
void bitmap_init(struct bitmap *btmp)
{
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}
bool bitmap_scan_test(struct bitmap *btmp, uint32_t bit_idx)
{
uint32_t byte_idx = bit_idx / 8;//向下取整用于数组索引。
uint32_t bit_odd = bit_idx % 8; //取余用索引数组内的位
return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
}
int bitmap_scan(struct bitmap *btmp, uint32_t cnt)
{
uint32_t idx_byte = 0;
/*先字节比较*/
while((0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)){
//该字节无空位,去下一个字节
++idx_byte;
}
ASSERT(idx_byte < btmp->btmp_bytes_len);
if(idx_byte == btmp->btmp_bytes_len){ //找不到可用空间
return -1;
}
//某字节有空位,则依次查找
int idx_bit = 0;
while((uint8_t)(BITMAP_MASK << idx_bit & btmp->bits [idx_byte]){
++idx_bit;
}
int bit_idx_start = idx_byte * 8 + idx_bit;
if(cnt == 1){
return bit_idx_start;
}
uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start);
//记录还有多少位可以判断
uint32_t next_bit = bit_idx-start + 1;
uint32_t count = 1;
bit_idx_start = -1;
while (bit_left -- > 0){
if(!(bitmap_scan_test(btmp, next_bit))){
count++;
}else{
count = 0;
}
if(count == cnt){
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
return bit_idx_start;
}
/*将位图bit_idx设置为value*/
void bitmap_set(struct bitmap *btmp, uint32_t bit_idx, int8_t value)
{
ASSERT((value == 0) || (value == 1));
uint32_t byte_idx = bit_idx / 8;
uint32_t bit_odd = bit_idx % 8;
if(value){
btmp->bits[byte_idx] != (BITMAP_MASK << bit_odd);
}else{
btmp->bits[byte_idx] &= (BITMAP_MASK << bit_odd);
}
}
分页机制下有了虚拟、物理这两种地址,们本节所讨论的就是有关这两类地址的内存池规划问题。
创建虚拟内存地址池和物理内存地址池
内存的规划
分为内核内存池和用户内存池。
内存池中的内存单位大小是 4KB.
所以任务都有各自的 4GB,需要为所有任务维护它们自己的虚拟地址池,一个任务一个。
kernel / memory.h
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"
/*虚拟地址池,用于虚拟地址管理*/
struct virtual_addr{
struct bitmap vaddr_bitmap; // 虚拟地址用到的位图结构
uint32_t vaddr_start; // 虚拟地址起始地址
};
extern struct pool kernel_pool, user_pool;
void mem_init(void);
#endif
kernel / memory.c
#include "memory.h"
#include "stdint.h"
#include "print.h"
#define PG_SIZE 4096
/*************** 位图地址 ******************
* 因为0xc009f00 是内核主线程栈顶, 0xc009e000 是内核主线程的 pcb
* 一个页框大小的位图可表示 128MB 内存,位图位置安排在地址 0xc009a000,
* 这样本系统最大支持 4个页框的位图,即 512 MB
*/
#define MEM_BITMAP_BASE 0xc009a000
/* 0xc0000000 是内核从虚拟地址 3G 起,
0x100000 指跨过低端 1MB 内存,使虚拟地址在逻辑上连续。*/
#define K_HEAP_START 0xc0100000
/* 内存池结构,生成两个实例,用于管理内核和用户内存池*/
struct pool{
struct bitmap pool_bitmap; //本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
};
struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构用来给内核分配虚拟地址
/*初始化内存池*/
static void mem_pool_init(uint32_t all_mem)
{
put_str(" mem_pool_init start\n");
uint32_t page_table_size = PG_SIZE * 256;
/*页表大小 = 页目录表(1页) + 第0和第768页目录框指向同一个页表(第1个页表)+
第 769~1022个页目录项共指向 254 个页表(254页),共 256 个页框*/
uint32_t used_mem = page_table_size + 0x100000;
//0x100000 为低端 1MB 内存。
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE;
/*1页为4KB,不管总内存是不是 4k 的倍数,
*对于以页为单位的内存分配策略,不足一页的内存不用考虑了*/
uint16_t kernel_free_pages = all_free_pages / 2;
uint16_t user_free_pages = all_free_pages - kernel_free_pages;
/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
uint32_t kbm_length = kernel_free_pages / 8;
// Kernel BitMap 的长度,位图中的一位表示一页,以字节为单位
uint32_t ubm_length = user_free_pages / 8;
// User BitMap 的长度
//Kernel pool start,内核内存池的起始地址。
uint32_t kp_start = used_mem;
//User Pool start, 用户内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
user_pool.pool_size = user_free_pages * PG_SIZE;
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
/********* 内核内存池和用户内存池位图 ***********
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节,
* 所以改为指定一块内存来生成位图。
***********************************************/
//内核使用的最高 0xc009f000,这是主线程的栈地址
//(内核的大小预计为70KB左右)
//32MB内存占有的位图为 2KB
//内核内存池的位图先定在 MEM_BITMAP_BASE (0xc009a000) 处
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
/* 用户内存池的位图紧跟在内核内存池位图之后 */
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
/********************输出内存池信息**********************/
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str("user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");
/* 将位图置于 0*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
put_str("bitmap_init2\n");
/* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
// 用于维护内核堆的虚拟地址,所以要和内核内存池大小一致
/* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done\n");
}
/*内存管理部分初始化入口*/
void mem_init()
{
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t *)(0xb00));
mem_pool_init(mem_bytes_total);// 初始化内存池
put_str("mem_init done\n");
}
还有一点:
这里的 total_mem_bytes 一定要放在这个位置上,恰好是 地址 0xb000。
因为 mem_init() 中要用到这个地址。上面存放的是内存大小。
loader.asm
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0;预留60个描述符的空位
;选择子
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
;============= 读取内存数
total_mem_bytes dd 0 ;4 保存内存容量
;gdt 指针
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0
ards_nr dw 0
/* ...略 */
kernel / init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
/*负责初始化所有模块*/
void init_all()
{
put_str("init_all\n");
idt_init(); //初始化中断
//timer_init();
mem_init();
}
kernel / main.c
#include "print.h"
#include "init.h"
#include "debug.h"
int main(void){
put_str("I am kernel\n");
init_all();
//ASSERT(1==2);
//asm volatile("sti");
while(1);
return 0;
}
运行结果: