第6天 分割编译与中断处理
第6天 分割编译与中断处理
2020.3.31
1. 分割源文件(harib03a)
开发至昨天,bootpack.c的长度已经有将近300行了。一个源文件太长可不是一件好事情。 因此,需要将bootpack.c分割成为几部分。
源文件分割的利与弊:
- 优点:
- 按照处理内容进行分类,如果分得好,将来修改时,可维护性高。
- 如果Makefile写得好,只需要编译修改过的文件,就可以提高make的速度。
- 单个源文件都不长。多个小文件对程序员的友好性比一个大文件好得多。
- 看起来很酷
- 缺点:
- 源文件数量增加
- 分类分不好反而增加寻找代码的难度
- 优点:
我们根据功能进行如下分割:
注意,做了如上分割以后,graphic.c如果想用naskfunc.nas定义的函数,那么就需要像bootpack.c一样加上声明。虽然bootpack.c里面有这些的声明,但是在编译graphic.c的时候,编译器并不知道有bootpack.c的存在。
修改Makefile:
源文件的编译流程应该是这样的:
(bootpack.c->bootpack.obj的完整过程是:bootpack.c->bootpack.gas->bootpack.nas->bootpack.obj)变成bootpack.bim以后就和原来的流程差不多了。
(bootpack.bim->boopack.hrb->haribote.sys->haribote.img)
make run
没有发生错误。
2. 整理Makefile(harib03b)
修改Makefile以后,Makefile又变得太长了。
graphic.c->graphic.obj、dsctbl.c->dsctbl.obj和bootpack.c->bootpack.的编译过程十分相似。
使用一般规则归纳(Makefile节选):
%.gas : %.c Makefile$(CC1) -o $*.gas $*.c%.nas : %.gas Makefile$(GAS2NASK) $*.gas $*.nas%.obj : %.nas Makefile$(NASK) $*.nas $*.obj $*.lst
注意:make.exe会先寻找普通生成的规则,如果没有找到,就会尝试使用一般规则。所以,一般规则和普通生成规则有冲突也没问题。普通规则优先级比一般规则高。
Makefile变短了。此时
make run
没有问题。
3. 整理头文件(harib03c)
源文件行数如下:
- graphic.c 187行
- dsctbl.c 67行
- bootpack.c 81行
- 合计 335行
显然,三个源文件的行数比原先的一个源文件行数280行多了不少。
使用头文件
bootpack.h
将函数声明归纳起来,这样就可以去除掉重复的部分了。/* asmhead.nas */ struct BOOTINFO { /* 0x0ff0-0x0fff */char cyls; /* 启动区读硬盘读到何处位置 */char leds; /* 启动时键盘的LED状态 */char vmode; /* 显卡模式为多少位色彩 */char reserve; /*保留位*/short scrnx, scrny; /* 屏幕分辨率 */char *vram; }; #define ADR_BOOTINFO 0x00000ff0 /*BOOTINFO在内存中的起始地址*//* naskfunc.nas */ void io_hlt(void); void io_cli(void); void io_out8(int port, int data); int io_load_eflags(void); void io_store_eflags(int eflags); void load_gdtr(int limit, int addr); void load_idtr(int limit, int addr);/* graphic.c */ void init_palette(void); void set_palette(int start, int end, unsigned char *rgb); void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1); void init_screen8(char *vram, int x, int y); void putfont8(char *vram, int xsize, int x, int y, char c, char *font); void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s); void init_mouse_cursor8(char *mouse, char bc); void putblock8_8(char *vram, int vxsize, int pxsize,int pysize, int px0, int py0, char *buf, int bxsize); #define COL8_000000 0 #define COL8_FF0000 1 #define COL8_00FF00 2 #define COL8_FFFF00 3 #define COL8_0000FF 4 #define COL8_FF00FF 5 #define COL8_00FFFF 6 #define COL8_FFFFFF 7 #define COL8_C6C6C6 8 #define COL8_840000 9 #define COL8_008400 10 #define COL8_848400 11 #define COL8_000084 12 #define COL8_840084 13 #define COL8_008484 14 #define COL8_848484 15/* dsctbl.c */ struct SEGMENT_DESCRIPTOR {short limit_low, base_low;char base_mid, access_right;char limit_high, base_high; }; struct GATE_DESCRIPTOR {short offset_low, selector;char dw_count, access_right;short offset_high; }; void init_gdtidt(void); void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar); void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar); #define ADR_IDT 0x0026f800 #define LIMIT_IDT 0x000007ff #define ADR_GDT 0x00270000 #define LIMIT_GDT 0x0000ffff #define ADR_BOTPAK 0x00280000 #define LIMIT_BOTPAK 0x0007ffff #define AR_DATA32_RW 0x4092 #define AR_CODE32_ER 0x409a
- 这个头文件中罗列除了函数的定义、常量宏定义、结构体定义、还说明了他们在那个文件中。因此,bootpack.h就像目录一样,查找函数位置、常量位置等十分方便。
- 在编译graphic.c的时候,我们要让编译器去读这个头文件,做法是在graphic.c的前面加上
#include "bootpack.h"
编译器在见到这一行,就将这行替换成为指定文件的内容,然后编译。
同理,dsctbl.c和bootpack.c中也要这样做。 - 小知识点:
#include "文件名.h"
:双引号表示该头文件和源文件在同一个文件夹下。#include <文件名.h>"
:尖括号表示该头文件位于编译器所提供的文件夹下。
- 许多地址常量和数据常量都写在该头文件里了,这样以后修它们直接在bootpack.h中修改就行了。
现在源文件的长度:
- bootpack.h 69行
- graphic.c 156行
- dsctbl.c 51行
- bootpack.c 25行
- 合计 301行
缩短了34行。
make run
一下也没什么问题。
4. 意犹未尽
读到这里,我发现,第5天的存疑四,在我的理解下是正确的。【Bingo!】
这里,有几个小问题需要再次说明:
- 指令
LGDT 地址addr
:将内存地址addr开始的6个字节读入GDTR寄存器中。 - GDTR的低16位,是段上限,它等于GDT的有效字节数-1。高32位是GDT的开始地址。
- 附上一张第5天的图片,理解load_gdtr起来可能比价容易:
- 指令
解决存疑1和存疑2
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { /*存疑1*/if (limit > 0xfffff) {/*从GDT图可以看出,limit占20位,所以最大是0xfffff。下面的代码因该是越界重置,但是没看懂为什么。*/ar |= 0x8000; /* G_bit = 1 */limit /= 0x1000;}sd->limit_low = limit & 0xffff; /*取低16位*/sd->base_low = base & 0xffff; /*取低16位*/sd->base_mid = (base >> 16) & 0xff; /*先右移16位再取低8位*/sd->access_right = ar & 0xff; /*取低8位*//*存疑2*/sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);sd->base_high = (base >> 24) & 0xff; /*右移24位,再取8位*/return; }
段8字节的内容:
- 段的大小
- 段的起始地址
- 段的管理属性
GDT中段的内容:
段上限,表示一个段有多少个字节(也就是段的大小)。段的上限最大是4GB【这里我们已经默认内存的大小是4GB】,也就是一个32位的数值,如果将这个数值直接放进去,那么这个数值和段的起始地址一共占了8字节。这样就把整个段占满了。
因此,段上限只能使用20位(这一点从上图也可以看出)。那么,我们段的大小只能指定到1MB为止。
这里,嘤特尔的大叔们又响了一个方法:他们在段的属性里设了一个标志位,叫做
Gbit
。如果这个标志位是1的话,段上限的单位解释成为页(page),在电脑的CPU中,1页=4KB,而不解释成字节B。 这样,就能用20位指定4GB的内存了。void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
- sd是GDT在内存中的起始地址。
- limit是无符号32位整数,其019位对应GDT中的019位。
- base的031位对应GDT中的031位。
- ar的0~15位的格式是:xxxx0000xxxxxxxx``,其中x是0或1.
- GDT中段属性只有12位,上述的0000无用。
- ar的高4位是
GD00
:G代表Gbit,D代表模式,0是16位模式,1是32位模式。 - 除了运行80286程序,D通常是1.
- ar的低8位:
- 00000000(0x00):未使用的记录表。
- 10010010(0x92):系统专用,可读写的段。不可执行。
- 10011010(0x9a):系统专用,可执行的段。可读不可写。
- 11110010(0xf2):应用程序用,可读写的段。不可执行。
- 11111010(0xfa): 应用程序用,可执行的段。可读不可写。
此时,我们再来看代码:set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092); set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
神秘的数字
0x4092
和0x409a
是不是就明了了。
存疑1解答
/*存疑1*/ if (limit > 0xfffff) {ar |= 0x8000; /* G_bit = 1 */limit /= 0x1000; }
- 当limit大于20位的时候,limit的单位解释成为页,将limit数值缩小(4KB=4096B=0x1000B)0x1000倍。
- 设此时ar=xxxx0000xxxxxxxx(只看其有含义的0~15位),ar |= 0x8000等价于ar = xxxx0000xxxxxxxx | 100000000000 = 1xxx0000xxxxxxxx。这样ar的高4位GD00中的G就是1了。
存疑2解答
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
- GDT结构体中变量与GDT设定的对应关系
- ar的015位对应着上图的4055位。 limit的1619位对应着上图的4851位。
- limit_high由Flags和Limit(16:19)组成。那么上述代码也就明了了。
- GDT结构体中变量与GDT设定的对应关系
5. 初始化PIC(harib03d)
PIC,programmable interrupt controller,可编程中断控制器。
CPU单独只能处理一个中断信号,这不够用,所以IBM的大叔们在设计电脑的时候,就在主板上增设了几个辅助芯片,现如今,这几个辅助芯片已经集成在一个芯片组里了。
PIC是将8个中断信号(IRQ, interrupt request) 集成1个终端信号的装置。
PIC监视着输入管脚的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号编程ON,并通知CPU。IBM的大叔们通过增加PIC来处理更多的中断信号。他们把中断信号设置成了15个,因此有两个PIC。
主PIC和从PIC:
- 主PIC:master PIC,与CPU直接相连,处理第0号到底7号中断。
- 从PIC:slave PIC,与主PIC相连,处理第8号到底15号中断。
- 关系如下:
PIC详细解读:8259A中断控制器
reference:https://baike.baidu.com/item/8259A中断控制器/3572337
PIC初始化程序(harib03d下的int.c的init_pic函数):
#include "bootpack.h"void init_pic(void) /* PIC初始化 */ {io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发模式 */io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由INT20-27接收 */io_out8(PIC0_ICW3, 1 << 2); /* PIC1由IRQ2连接 */io_out8(PIC0_ICW4, 0x01 ); /* 无缓冲区模式 */io_out8(PIC1_ICW1, 0x11 ); /* 边沿触发模式 */io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-2f接收 */io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全部禁止中断 */io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所欲中断 */return; }
- PIC0和PIC1分别代表主PIC和从PIC。PIC内部有很多寄存器,用端口号对彼此进行区别,以决定是写入的哪一个寄存器。 PIC是外部设备,需要使用OUT指令进行操作。
- 至于为什么这样设置具体的端口号码,上方PIC详细解读:8259A中断控制器有详细解读。
- bootpack.h中有关int.c的声明:
/* int.c */ void init_pic(void); #define PIC0_ICW1 0x0020 #define PIC0_OCW2 0x0020 #define PIC0_IMR 0x0021 #define PIC0_ICW2 0x0021 #define PIC0_ICW3 0x0021 #define PIC0_ICW4 0x0021 #define PIC1_ICW1 0x00a0 #define PIC1_OCW2 0x00a0 #define PIC1_IMR 0x00a1 #define PIC1_ICW2 0x00a1 #define PIC1_ICW3 0x00a1 #define PIC1_ICW4 0x00a1
- 8259A系列芯片的编程有关设定:
- bootpack.h中有关int.c的声明:
简单介绍PIC的寄存器:
- 他们都是8位寄存器。
- IMR是interrupt mask register的缩写,意思是“中断屏蔽寄存器”。它的8位分别对应8路IRQ信号。如果某1位的值是1,那么对应的IRQ信号就会被屏蔽,PIC就忽视该路的IRQ信号。(这主要是因为,正在对中断设定进行更改时,如果再接受别的中断,那么就会引起混乱。)
- ICW是initial control word的缩写,(虽然是word,但注意,ICW是一个字节的口令)意思是,初始化控制程序。
- ICW有4个,编号1~4,共有4字节的数据。
- 它们的初始化在 PIC详细解读:8259A中断控制器 均有详细说明,此处不再赘述。
- 值得一提的是,ICW1和ICW4基本不变。而ICW3的设定是有关主从连接的设定。对主PIC而言,第几号IRQ与从PIC相连,是用8位来设定的,如果把这8位都设定成1,那么主PIC就能驱动8个从PIC,进而就有64个IRQ。但是,这里我们只使用两个PIC。
- 8259A系列芯片的PIC寄存器:
ICW2:
- ICW2决定了IRQ以哪一号中断通知CPU。
- 中断发生后,如果CPU可以受理这个中断,CPU就会命令PIC发送2个字节的数据。CPU与PIC用IN或者OUT进行数据传送时,有数据信号线连在一起。PIC就是利用这个数据信号线发送数据的。发送过来的数据是
0xcd 0x??
这2个字节,由于电路设计的原因,这两个字节的数据在CPU看来,与从内存读进来的程序一样。这恰恰就是把数据当做程序来执行的情况。 这里的0xcd
就是调用BIOS时使用的INT指令(INT指令:引发中断) 所以,CPU上了PIC的当,按照PIC所希望的中断号执行了INT指令。
INT 0x200x2f接收中断信号IRQ015。
make run
,正常运行,但画面没什么变化。
6. 中断处理程序的制作(harib03e)
鼠标中断信号是IRQ12,键盘中断信号是IRQ1,那么我们需要分别编写用于
INT 0x2c
和INT 0x21
的中断处理程序(handler),即发生中断时需要调用的程序。harib03e下的int.c中的inthandler21函数(键盘中断处理程序):
void inthandler21(int *esp) /* 来自PS/2键盘的中断 */ {struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");for (;;) {io_hlt();} }
- 这个函数只是显示一条信息,然后保持在待机状态。
- 这个函数接受了esp指针的值,但是这里还用不到,不必在意。
harib03e下的int.c中的inthandler2c函数(鼠标中断处理程序):
void inthandler2c(int *esp) {struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 2C (IRQ-12) : PS/2 mouse");for (;;) {/*今天的程序不需要return,到这里待机就OK了*/io_hlt();} }
- 和inthandler21函数大同小异。
harib03e下的int.c中的inthandler27函数:
void inthandler27(int *esp) {io_out8(PIC0_OCW2, 0x67);return; }
- 对于一部分机种来言,随着PIC的初始化,会产生一次IRQ7中断,如果不对该中断处理程序执行STI(设置中断标志位),OS就会启动失败。***(第7天会详细讲一下。)***
中断处理完成以后,不能执行return(RET指令),而是必须执行IRETD指令。 因此,需要修改naskfunc.nas.
修改naskfunc.nas(新增代码节选):
GLOBAL _asm_inthandler21, _asm_inthandler27, _asm_inthandler2cEXTERN _inthandler21, _inthandler27, _inthandler2c_asm_inthandler21:PUSH ESPUSH DSPUSHADMOV EAX,ESPPUSH EAXMOV AX,SSMOV DS,AXMOV ES,AXCALL _inthandler21POP EAXPOPADPOP DSPOP ESIRETD
这是键盘程序,鼠标程序和它类似。
PUSH:将数据压入栈顶
PUSH EAX
相当于代码:ADD ESP,-4 MOV [SS:ESP],EAX
其中,SS是栈段寄存器,SS:ESP表示一个地址,这个地址永远指向栈顶元素。
POP:将数据弹出栈
PUSH EAX
相当于代码:MOV [SS:ESP],EAX ADD ESP,4
先PUSH再POP可以恢复寄存器原先的值。
PUSHAD
相当于:PUSH EAX PUSH ECX PUSH EDX PUSH EBX PUSH ESP PUSH EBP PUSH ESI PUSH EDI
POPAD
相当于按以上相反的顺序,把寄存器们原先的值POP出来:POP EDI POP ESI POP EBP POP ESP POP EBX POP EDX POP ECX POP EAX
那么函数asm_inthandler21只是将寄存器中的值保存到栈中,然后将DS和ES调整到和SS一致,再调用函数inthandler21,返回以后再将所有寄存器的值返回到原来的值,最后再执行IRETD。
只有这样,才能保证原来的函数能够继续进行下去。
关于为什么要设置DS和ES与SS一致,这是C语言的规定。
CALL指令
:调用函数的指令。使用EXTERN声明inthandler21是用来通知nask:inthandler21在别的源文件里,别搞错了。
将asm_inthandler21注册到IDT当中去(dsctbl.c的init_gdtidt函数中添加代码):
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32); set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32); set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
- asm_inthandler21注册在idt的0x21号。如果发生中断,CPU就会自动调用asm_inthandler21.
- 2*8表示的是asm_inthandler21属于哪一个段,asm_inthandler21属于第2个段,乘以8是因为低3位有别的意思,这里的低3位必须是0.
- 段号为2的段是:
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
说明这个段正好涵盖了整个bootpack.hrb.
- 最后的AR_INTGATE32将IDT的属性设置为0x008e.这表示是用于中断处理的有效设定。
harib03e下的bootpack.c中的HariMain的补充说明:
#include "bootpack.h" #include <stdio.h>void HariMain(void) {struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;char s[40], mcursor[256];int mx, my;init_gdtidt();init_pic();io_sti(); /*执行STI指令*/init_palette();init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);mx = (binfo->scrnx - 16) / 2; my = (binfo->scrny - 28 - 16) / 2;init_mouse_cursor8(mcursor, COL8_008484);putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);sprintf(s, "(%d, %d)", mx, my);putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s);/*修改PIC的IMR,接受来自键盘和鼠标的中断*/io_out8(PIC0_IMR, 0xf9); /*接受键盘中断*/io_out8(PIC1_IMR, 0xef); /*接受鼠标中断*/for (;;) {io_hlt();} }
io_sti():执行STI指令,是CLI的逆指令。执行STI后,CPU的中断许可标志位IF变为1,CPU接受来自外部设备的中断。CPU的中断信号只有1根,所以IF只有一个。
这里,还得说明一点:
IRQ0->IRQ7:低位->高位
IRQ8->IRQ15:低位->高位
这样,只要按下键盘上的某个键,或者动一动鼠标,中断信号就会传到CPU,然后CPU停下手中的工作去执行相应的中断处理程序,输出信息。
make run
:按下键盘上的a:
移动鼠标:
(很不幸,没有任何变化)
7. 戒骄戒躁
- 大致往后面看了一下,大概得到第8天才能实现鼠标的移动。
- 第6天的东西其实并不算难,现在是2020.4.1 16:50,我却看了2天。
- 往后大概浏览了一点儿,感觉还有好多东西没有完成。心情顿时灰暗。
- 这大概就是摸索前进中的“黑暗时刻”吧,前方只能看见一丁点儿的光,眼前却还要经历很长的黑暗。
- 戒骄戒躁,静下心来,完成当下的任务才是王道。
- 走得很远,别忘记为了什么而出发。人得学会坚持。
- 2020.04.01 16:58 愚人节快乐!
第6天 分割编译与中断处理相关推荐
- 第6天:分割处理与中断处理
6.1.分割处理 6.1.1.bootpack.c拆分 6.1.2.MakeFile整理 使用了一般规则 %.gas : %.c Makefile$(CC1) -o $*.gas $*.c%.nas ...
- 《30天自制操作系统》前言、目录、样章欢迎阅读!
编著推荐: 只需30天从零开始编写一个五脏俱全的图形操作系统 如果肯坚持,没有什么不可以!祝所有读到这篇文章的人都能写出好的操作系统! 内容简介: 自己编写一个操作系统,是许多程序员的梦想.也许有人曾 ...
- 自制操作系统——第一周
文章目录 第一周 第一天-从计算机结构到汇编程序入门 HelloWorld 引入汇编 添加注释 第二天-汇编语言学习与Makefile入门 文本编辑器 初识汇编 寄存器 数据大小 指令 制作启动区 第 ...
- 索骥馆-DIY操作系统之《30天自制操作系统》扫描版[PDF]
内容简介: <30天自制操作系统>是一本兼具趣味性.实用性与学习性的操作系统图书.作者从计算机的构造.汇编语言.C语言开始解说,让读者在实践中掌握算法.在这本书的指导下,从零编写所有代码, ...
- 粗略阅读haribote OS 3
2016.09.04 -09.16 <haribote>-川合秀实 个人笔记. #1 haribote 09.04 功能.从头到尾开发一个能够显示任意多的窗口.实现鼠标光标控制.能够同时运 ...
- 关于API的设计和需求抽象
一,先来谈抽象吧,因为抽象跟后面的API的设计是息息相关的 有句话说的好(不知道谁说的了):计算机科学中的任何问题都可以抽象出一个中间层就解决了. 抽象是指在思维中对同类事物去除其现象的.次要的方面, ...
- 二. 征服C指针:C如何使用内存
虚拟内存 现代OS都会给应用程序每个进程分配独立的虚拟地址空间.这样做的目的是为了保证安全,防止应用程序破坏内存空间..这和 C 语言本 身并没有关系,而是操作系统和 CPU 协同工作的结果.即:应用 ...
- 程序是怎么跑起来的(中)
压缩数据 文件以字节为单位保存 从物理上对磁盘进行读写时是以扇区(512字节)为单位的,但是另一方面,程序则可以在逻辑上以字节为单位对文件的内容进行读写 在任何情况下,文件中的数据都是连续存储的 RL ...
- 《征服C指针》——读书笔记(4)
一.函数与字符串常量 1. 只读内存区域 如今的大多数操作系统都是将函数自身和字符串常量汇总配置在一个只读内存区域的. 函数本身一旦被写定后基本不再需要改写,所以它被配置在内存的只读区域.此外,如果执 ...
最新文章
- Mybatis 3 返回布尔值,需要注意的地方
- lisp改图元字体式样_一个更改尺寸类型的LISP程序
- 服务器ios文件,ios 文件到服务器
- 小米POCO X3今日亮相:首发骁龙732G后置6400万四摄
- preg_match_all使用实例
- promotion failed 和 Concurrent Mode Failure的区别
- H3C中标苏州教育城域网改造项目
- 【资源】同济线性代数教材(第五版)
- amd编码器 hevc_Bandicam支持Nvidia NVENC编码器(H264, HEVC) - Bandicam(班迪录屏)
- Hyper-V虚拟机安装win10系统 2021-10-16
- 银行卡收单____商户费率_代理商分润
- b站上的计算机课程有哪些,B站课程排行榜,这届大学生最爱学什么?
- 常用的正则字母大小写转换
- Kubernetes(七)Pod进阶之Downward API和PodPreset
- 【2020-10-28】DS12C887+驱动
- Euraka服务注册篇
- Java线程同步-模拟买票
- 从STM32F407到AT32F407(一)
- 江西用计算机写作文说课稿,信息技术说课稿范文(精选5篇)
- 重点人员数据分析管控平台建设,重点人员系统开发