安装 Ubuntu Linux 20.04
命令行方式的编译、调试、运行操作系统
命令模式的基本结构和概念:实现需要的所有操作
进入命令模式:GNOME菜单->附件->终端
命令行终端提示符:表示计算机已就绪,等待用户输入操作指令。此时输入任何指令按回车后,该指令将会被提交到计算机运行
常用指令:
选项:
其他基本指令:
控制流程
输入/输出
重定向
管道:把几个简单命令联合成为复杂的功能
|
grep -i command < myfile | sort > result.text
后台进程:要启动一个进程到后台,追加 & 到命令后面
sleep 60 &
睡眠命令在后台运行,宁依然可以与计算机交互。除了不同步启动命令外,
如果一个命令将占用很多时间,想让其放入后台运行
sleep 60
< ctrl z> # 停止
bg #转入后台
fg # 转回前台
ctrl c
:杀死一个前台程序
环境变量
获得软件包
命令行获取软件包
apt-get
: 自动从互联网软件库中搜索、安装、升级以及卸载软件或者操作系统
一般需要root 执行权限
语法 sudo apt-get install gcc
常见 apt 命令:
apt-get install # 下载 以及所依赖的软件包,同时进行软件包的安装或者升级
apt-get remove #移除以及所依赖的软件包
apt-cache search #搜索满足pattern 的软件包
apt-cache show/showpkg #显示软件包 的完整描述
解决 apt 下载速度太慢:
图形界面软件包获取
菜单栏->系统管理 -> 新德里软件包管理器
配置升级源
Ubuntu的软件包获取依赖升级源,可以通过修改 “/etc/apt/sources.list” 文件来修改升级源(需要 root 权限);或者修改新立得软件包管理器中 “设置 > 软件库”。
查找帮助文件
man
man 命令
编辑器
gnome
Vim
下载安装VIm sudo spt-get install vim
查看 vim 版本命令 vim --version
编辑配置文件至 ~/.vimrc
在启动vim时,当前用户根目录下的.vimrc文件会被自动读取,该文件可以包含一些设置甚至脚本,所以,一般情况下把.vimrc文件创建在当前用户的根目录下比较方便
set nocompatible set encoding=utf-8 set fileencodings=utf-8,chinese set tabstop=4 set cindent shiftwidth=4 set backspace=indent,eol,start autocmd Filetype c set omnifunc=ccomplete#Complete autocmd Filetype cpp set omnifunc=cppcomplete#Complete set incsearch set number set display=lastline set ignorecase syntax on set nobackup set ruler set showcmd set smartindent set hlsearch set cmdheight=1 set laststatus=2 set shortmess=atI set formatoptions=tcrqn set autoindent
exuberant-ctags: 为程序语言对象生成索引,其结果能够被一个文本编辑器或者其他工具简捷迅速的定位
使用 vim 编辑一个 .c 文件
用 gcc 编译文件:gcc -Wall hello.c -o hello
该命令将文件‘hello.c’中的代码编译为机器码并存储在可执行文件 ‘hello’中。机器码的文件名是通过 -o 选项指定的。该选项通常作为命令行中的最后一个参数。如果被省略,输出文件默认为 ‘a.out
-Wall
: 开启编译器几乎所有常用的警告
运行可执行文件:输入可执行文件的路径
./hello
该命令将可执行文件载入内存,并使CPU开始执行其包含的指令
./ 当前目录
因此
./hello
: 载入并执行当前目录下的可执行文件 hello
Ucore 用的是 AT&T 格式的汇编
%
$
0x $
$
:引用符号地址section:[base+index*scale+displacement]
section:displacement(base,index,scale)
与 Intel格式的汇编的不同
* 寄存器命名原则
AT&T: %eax Intel: eax
* 源/目的操作数顺序
AT&T: movl %eax, %ebx Intel: mov ebx, eax
* 常数/立即数的格式
AT&T: movl $_value, %ebx Intel: mov eax, _value
把value的地址放入eax寄存器
AT&T: movl $0xd00d, %ebx Intel: mov ebx, 0xd00d
* 操作数长度标识
AT&T: movw %ax, %bx Intel: mov bx, ax
* 寻址方式
AT&T: immed32(basepointer, indexpointer, indexscale)
Intel: [basepointer + indexpointer × indexscale + imm32)
OS 工作于保护模式下时,用 32 位线性地址,所以在计算地址时不用考虑 segment :offse
上式中的地址immed32+basepointer+indexpointer*indexscale
* 直接寻址
AT&T: foo Intel: [foo]
foo是一个全局变量。注意加上$是表示地址引用,不加是表示值引用。对于局部变量,可以通过堆栈指针引用。
* 寄存器间接寻址
AT&T: (%eax) Intel: [eax]
* 变址寻址
AT&T: _variable(%eax) Intel: [eax + _variable]
AT&T: _array( ,%eax, 4) Intel: [eax × 4 + _array]
AT&T: _array(%ebx, %eax,8) Intel: [ebx + eax × 8 + _array]
GCC 支持在 C++ 代码中嵌入汇编代码:GCC Inline ASM——GCC 内联汇编
GCC 提供了两内联汇编语句(Inline asm statements):
asm("statements");
"asm"
和“_asm_”
的含义一致: 声明一个内联汇编表达式
#define _asm_ asm
若有多行汇编,每行最后加 \n\t
gcc 在处理汇编时,把asm()的内容打印到汇编文件中,格式控制字符是必要的
asm("movl %eax, %ebx");
asm("xorl %ebx, %edx");
asm("movl $0, _boo);
在上面的例子中,由于我们在内联汇编中改变了 edx 和 ebx 的值,但是由于 gcc 的特殊的处理方法,即先形成汇编文件,再交给 GAS 去汇编,所以 GAS 并不知道我们已经改变了 edx和 ebx 的值,如果程序的上下文需要 edx 或 ebx 作其他内存单元或变量的暂存,就会产生没有预料的多次赋值,引起严重的后果。对于变量 _boo也存在一样的问题。为了解决这个问题,就要用到扩展 GCC 内联汇编语法。
参考博客
#define read_cr0() ({ \ unsigned int __dummy; \ __asm__( \ "movl %%cr0,%0\n\t" \ :"=r" (__dummy)); \ __dummy; \ })
GCC 扩展内联汇编基本格式
asm [volatile] (Assembler Template
: Output operands
[ : Input Operands
[ : Clobbers ] ] )
asm 代表汇编代码的开始
[volatile] : 可选项,避免 asm 指令被删除、移动或组合
指令“movl %%cr0,%0\n\t”
数字前缀:%1
, 表示使用寄存器的样板操作数
%%cr0
: 两个 % ,表示用到具体的寄存器Output Operands list:输出部分,用以规定对输出变量(目标操作数)如何与寄存器结合的约束(constraint)。输出部分可以有多个约束,以逗号分开
每个约束语法:“=约束字母” 关于变量结合的约束
:"=r" (_dummy)
:"=m" (_dummy)
几个主要的约束字母
字母 | 含义 |
---|---|
m, v, o | 内存单元 |
R | 任何通用寄存器 |
Q | 寄存器eax, ebx, ecx,edx之一 |
I, h | 直接操作数 |
E, F | 浮点数 |
G | 任意 |
a, b, c, d | 寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl |
S, D | 寄存器esi或edi |
I | 常数(0~31) |
Input Operand list: 输入部分
=
[
参考博客
内联函数:在C语言中,指定编译器将一个函数代码直接复制到调用其代码的地方执行。
内联汇编:用汇编语句写成的内联函数
在扩展形式中,可以指定操作数,选择输入输出寄存器,指明要修改的寄存器列表
形式:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
assembler template
: 汇编指令部分
括号内的operands:C 语言表达式中常量字符串,不同部分之间用冒号分隔
相同部分语句中的每个小部分用逗号分隔
最多指定10个操作数
若没有 output operands ,有input operands,需要保留output 前的冒号
asm ( "cld\n\t"
"rep\n\t"
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
例子:用汇编代码把a的值赋给b
int a=10, b;
asm ( "movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
\n\t
格式"constraint" (C expr) //"=r"(result)
asm 内部使用C语言字符串作为操作数
操作数都要放在双引号内
constraint 和 修饰都放在双引号内
constraint:指定操作数的寻址类型(内存寻址或者寄存器寻址),也用来指明使用那个寄存器
若有多个操作数,使用逗号隔开
在汇编模板部分,按顺序用数字去引用操作数
输出操作数表达式必须是左值,输入操作数不一定是
例子:把一个数字乘以五
asm ( "leal (%1,%1,4), %0"
: "=r" (five_times_x)
: "r" (x)
);
输入操作数是 x,未指定具体使用那个寄存器
修改 constraint 部分内容,使得 GCC 固定使用同一个寄存器处理输入输出操作数,但未指定具体哪个寄存器
asm( "lea (%0,%0,4),%0"
: "=r" (five_times_x)
: "0" (x)
);
需要指定具体的寄存器
asm ( "leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (x)
: "c" (x)
);
不需要填写ClobberList
要求汇编代码必须在被放置的位置执行(不能被循环优化或移除循环)
禁止这些代码被移动或删除
asm volatile ();
寄存器操作数 constraints:r
操作数将被存储在通用寄存器中
asm ("movl %%eax, %0": "=r" (myval));
具体指定使用那个寄存器:
r | Register(s) |
---|---|
a | %eax, %ax, %al |
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %adl |
S | %esi, %si |
D | %edi, %di |
内存操作数constraint:m
匹配 constraint
target … :prerequisites …
command
…
…
##3.3 gdb 的使用
参考博客
gcc -g -Wall 原文件.c -o 输出的目标文件
gdb 可执行文件名
gdb 可执行文件名 core
gdb 可执行文件名
help
:查看命令种类
help 命令
:查看种类中的命令ps
查看正在运行的程序 ID,gdb PID
格式挂接正在运行的程序gdb
关联上源代码,并进行gdb,在 gdb 环境中 用 attach
命令来挂接进程 PID ,用 detach
取消挂接info program
查看程序是否在运行,进程号,被暂停原因c
/ continue
break
: 在进入指定函数时停住break
: 在指定行号停住break +offset
: 在当前行号前面的 offset 行停住break -offset
: 在当前行号后面 offset 行挺住break filename: linenum
: 在源文件 filename 的 linenum 行停住break filename: function
: 在源文件filename的function入口处同住break *address
: 在程序运行的内存地址处停住break
: 没有参数时,表示在下一条指令处停住break ... if
: 在条件成立时停住
...
是上述参数,conditon 表示条件info breakpoints [n]
info break [n]
观察点一般来观察某个表达式(或变量)的值是否变化,若变化则停住
watch
: 为表达式 expr 设置观察点rwatch
: 当表达式 expr 被读时,停住awatch
: 当表达式的值被读或写时,停住info watchpoints
: 列出所有观察点设置捕捉点来捕捉程序运行时的一些事件
catch
: 当event发生时,停住
tcatch
: 值设置一次捕捉点,当程序停住后,该店被自动删除停止点即上述三类
clear
: 清除所有已定义的停止点clear
/ clear
: 清除所有设置在函数上的停止点clear
/ clear
: 清除所有设置在指定行上的停止点delete [breakpoints] [range...]
: 删除指定的断点,断点号,若不指定断点号则删除所有断点。range 表示断点号范围disable [breakpoints] [range]
:enable [breakpoints] [range]
enable [breakpoints] once range...
enable [breakpoints] delete range ...
断点条件设置好后,用 conditon 命令修改条件(仅break和watch支持if)
condition
: 修改断点号为 bnum 的停止条件为 exprcondition
: 清除断点号为 bnum 的停止条件当程序被停住,可以用 continue 命令恢复程序运行直到结束,或下一个断点,
也可以使用 step 或 next 命令单步跟踪程序
continue [ignore-count]
/ c / fg
step
next
set step-mod
set step-mod on
set step-mod off
finish
until / u
stepi
nexti
功能强大的调试程序
必须先用 -g 或 -ggdb 编译选项编译源文件,才能使用 gdb 调试程序
gdb progname
在 gdb 提示符处键入 help ,
help 命令
:该命令的详细
gdb 常用命令
break FILENAME:NUM | 在特定源文件特定行上设置断点 |
---|---|
clear FILENAME:NUM | 删除设置在特定源文件特定行上的断点 |
run | 运行调试程序 |
step | 单步执行调试程序,不会直接执行函数 |
next | 单步执行调试程序,会直接执行函数 |
backtrace | 显示所有的调用栈帧。该命令可用来显示函数的调用顺序 |
where continue | 继续执行正在调试的程序 |
display EXPR | 每次程序停止后显示表达式的值,表达式由程序定义的变量组成 |
file FILENAME | 装载指定的可执行文件进行调试 |
help CMDNAME | 显示指定调试命令的帮助信息 |
info break | 显示当前断点列表,包括到达断点处的次数等 |
info files | 显示被调试文件的详细信息 |
info func | 显示被调试程序的所有函数名称 |
info prog | 显示被调试程序的执行状态 |
info local | 显示被调试程序当前函数中的局部变量信息 |
info var | 显示被调试程序的所有全局和静态变量名称 |
kill | 终止正在被调试的程序 |
list | 显示被调试程序的源代码 |
quit | 退出 gdb |
参考博客
sudo apt-gett install qemu-system
参考博客
qemu
无反应qemu-system-x86_64说明安装成功,需要对其进行链接:
sudo ln -s /usr/bin/qumu-system-x86_64 /usr/bin/qemu`默认安装路径:/usr/local/bin
运行命令:qemu
qemu 运行多参数格式qemu [options] [disk_image]
部分参数:
`-hda file' `-hdb file' `-hdc file' `-hdd file'
使用 file 作为硬盘0、1、2、3镜像。
`-fda file' `-fdb file'
使用 file 作为软盘镜像,可以使用 /dev/fd0 作为 file 来使用主机软盘。
`-cdrom file'
使用 file 作为光盘镜像,可以使用 /dev/cdrom 作为 file 来使用主机 cd-rom。
`-boot [a|c|d]'
从软盘(a)、光盘(c)、硬盘启动(d),默认硬盘启动。
`-snapshot'
写入临时文件而不写回磁盘镜像,可以使用 C-a s 来强制写回。
`-m megs'
设置虚拟内存为 msg M字节,默认为 128M 字节。
`-smp n'
设置为有 n 个 CPU 的 SMP 系统。以 PC 为目标机,最多支持 255 个 CPU。
`-nographic'
禁止使用图形输出。
其他:
可用的主机设备 dev 例如:
vc
虚拟终端。
null
空设备
/dev/XXX
使用主机的 tty。
file: filename
将输出写入到文件 filename 中。
stdio
标准输入/输出。
pipe:pipename
命令管道 pipename。
等。
使用 dev 设备的命令如:
`-serial dev'
重定向虚拟串口到主机设备 dev 中。
`-parallel dev'
重定向虚拟并口到主机设备 dev 中。
`-monitor dev'
重定向 monitor 到主机设备 dev 中。
其他参数:
`-s'
等待 gdb 连接到端口 1234。
`-p port'
改变 gdb 连接端口到 port。
`-S'
在启动时不启动 CPU, 需要在 monitor 中输入 'c',才能让qemu继续模拟工作。
`-d'
输出日志到 qemu.log 文件。
将用到的命令:
qemu -hda ucore.img -parallel stdip
: 使得ucore在qemu模拟的 x86 硬件环境中执行qemu -S -s -hda ucore.img -monitor stdio
: 用于与 gdb 配合进行源码调试qemu 中 monitor 的常用命令:
help | 查看 qemu 帮助,显示所有支持的命令。 |
---|---|
q|quit|exit | 退出 qemu。 |
stop | 停止 qemu。 |
c|cont|continue | 连续执行。 |
x /fmt addr xp /fmt addr | 显示内存内容,其中 ‘x’ 为虚地址,‘xp’ 为实地址。 参数 /fmt i 表示反汇编,缺省参数为前一次参数。 |
p|print’ | 计算表达式值并显示,例如 $reg 表示寄存器结果。 |
memsave addr size file pmemsave addr size file | 将内存保存到文件,memsave 为虚地址,pmemsave 为实地址。 |
breakpoint 相关: | 设置、查看以及删除 breakpoint,pc执行到 breakpoint,qemu 停止。(暂时没有此功能) |
watchpoint 相关: | 设置、查看以及删除 watchpoint, 当 watchpoint 地址内容被修改,停止。(暂时没有此功能) |
s|step | 单步一条指令,能够跳过断点执行。 |
r|registers | 显示全部寄存器内容。 |
info 相关操作 | 查询 qemu 支持的关于系统状态信息的操作。 |
single arg
: arg为参数,设置单步标志命令
single on
: 允许单步
cont
进行单步操作single off
: 禁止单步[]:~/lab1$ make
与 qemu 配合进行源代码级别的调试,需要先让 qemu 进入等待 gdb 调试器的接入并且还不能让 qemu 的CPU执行
qemu -S -s
:qemu 中的CPU并不会马上执行然后启动 gdb ,target remote 127.0.0.1:1234
连接到qemu
c
: qemu 继续执行
遇到的问题:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7sm4cLB-1617714592074)(E:\LearningNotes\TH操作系统\操作系统实验\lab0.assets\image-20210403211514163.png)]
为了使得 gdb 获知符号信息,需要指定调试目标文件,gdb 中 file ./bin/kernel
遇到的问题:
*
通过 gdb 可以对 ucore 代码进行调试
例如: 调试memset函数:
qemu -S -s -hda ./bin/ucore.img -monitor stdio
file
(gdb) set arch i8086
对于ucore无必要涉及
页机制和段机制有一定程度的功能重复,但Intel公司为了向下兼容,使得两者一直共存
80386 寄存器可分为8类:宽度都为 32 位
通用寄存器(General Register)
EAX(累加器)/EBX(基址寄存器)/ECX(计数器)/EDX(数据寄存器)/
ESI(源地址指针寄存器)/EDI(目的地址指针寄存器)/ESP(堆栈指针寄存器)/EBP(基址指针寄存器)
段寄存器(Segment Register): 都是 16 位的
指令指针寄存器(Instruction Pointer)
标志寄存器(Flag Register):
系统地址寄存器
控制寄存器
调试寄存器
测试寄存器
数据结构课程中
专门的成员变量 data
两个指向该类型的指针 next 和 prev
typedef struct foo {
ElemType data;
struct foo *prev;
struct foo *next;
} foo_t;
特点:
潜在问题:由于每种特定数据结构类型不一致,需要为每种特定数据结构类型 定义针对这个数据结构的特定链表插入、删除等操作
在uCore中,借鉴了 Linux 内核的双向链表实现:
struct list_entry_t {
struct list_entry_t *prev, *next;
};
链表节点 list_entry_t 没有包含数据域,而是在具体的数据结构中包含该链表节点
例如 lab 2 中的空闲内存块列表,空闲块链表的头指针定义为:
/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
每一个空闲块链表节点定义为:
/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
struct Page {
atomic_t ref; // page frame's reference counter
……
list_entry_t page_link; // free list link
};
通用双向循环链表结构
通用双向循环链表函数定义:
初始化:
uCore 只定义了链表节点,没有专门定义链表头
内联函数(inline function)list_init:
static inline void
list_init(list_entry_t *elm) {
elm->prev = elm->next = elm;
}
调用 list_init(free_area.free_list) ,
插入:
表头插入(list_add_after)
表尾插入(list_add_before)
由于双向循环链表的链表头 next 和 prev 分别指向链表中第一个和最后一个节点,两者实现区别并不大
uCore
list_add:
static inline void
__list_add(list_entry_t *elm, list_entry_t *prev, list_entry_t *next) {
prev->next = next->prev = elm;
elm->next = next;
elm->prev = prev;
}
表头插入:插入在listelm 后,即插在链表的最前位置
表尾插入:插入在 listelm->prev 之后,即插入在链表最后位置
删除
删除空闲块链表中的 Page 结构的链表节点时,调用 内联函数 list_del , list_del 进一步调用了_list_del 完成具体的删除操作
static inline void
list_del(list_entry_t *listelm) {
__list_del(listelm->prev, listelm->next);
}
static inline void
__list_del(list_entry_t *prev, list_entry_t *next) {
prev->next = next;
next->prev = prev;
}
如果要确保 被删除的节点 listelm 不在指向链表中其他节点,可以通过调用 list_init 来把 listelm 的pre、next 指针分别指向自身——可以通过 list_del_int 完成
访问链表节点所在的宿主数据结构
list_entry_t 通用双向循环链表 仅仅保存了某特定数据结构中链表节点成员变量的地址
如何通过这个链表节点成员变量访问到他的所有者(某特定数据结构的变量)
Linux 提供了针对该数据结构 XXX 的 leXXX 的宏
//free_area是空闲块管理结构,free_area.free_list是空闲块链表头
free_area_t free_area;
list_entry_t * le = &free_area.free_list; //le是空闲块链表头指针
while((le=list_next(le)) != &free_area.free_list) { //从第一个节点开始遍历
struct Page *p = le2page(le, page_link); //获取节点所在基于Page数据结构的变量
……
}
le2page 宏:
// convert list entry to page
#define le2page(le, member) \
to_struct((le), struct Page, member)