当前位置: 首页 > 工具软件 > cli-lab > 使用案例 >

uCore学习笔记-Lab0

张溪叠
2023-12-01

uCore实验笔记

Bash Shell常用指令

  • ls:查询文件列表
    ls 命令默认状态下将按首字母升序列出你当前文件夹下面的所有内容,但这样直接运行所得到的信息也是比较少的,通常它可以结合以下这些参数运行以查询更多的信息:
ls /          # 将列出根目录'/'下的文件清单.如果给定一个参数,则命令行会把该参数当作命令行的工作目录。换句话说,命令行不再以当前目录为工作目录。
ls -l         # 将给你列出一个更详细的文件清单.
ls -a         # 将列出包括隐藏文件(以.开头的文件)在内的所有文件
ls -h         # 将以 KB/MB/GB 的形式给出文件大小,而不是以纯粹的 Bytes.
  • pwd:查询当前所在目录
  • cd:进入其他目录
  • echo:在屏幕上输出字符(这是一个很有用的命令,它可以在屏幕上输入你指定的参数,也就是"**"中的内存,当然这里举的这个例子中它没有多大的实际意义,但随着你对 LINUX 指令的不断深入,就会发现它的价值所在)
  • cat:显示文件内容(也可以使用less或者more来显示比较大的文本文件内容)
  • cp:复制文件
  • mv:移动文件
    值得注意的是:在命令操作时系统基本上不会给你什么提示,当然,绝大多数的命令可以通过加上一个参数-v 来要求系统给出执行命令的反馈信息;
zhengdewu@zhengdewu-virtual-machine:~/Desktop$ mv -v file1.txt new_file.txt
`file1.txt' -> `new_file.txt'
  • touch:建立一个空文件

  • mkdir:建立一个目录

  • rm:删除文件/目录
     使用rm最好时加上-i参数:-i参数是让系统在执行删除操作前输出一条确认提示;i(interactive)也就是交互性的意思;

     原因::在使用命令操作时,系统假设你很明确自己在做什么,它不会给你太多的提示,比如你执行 rm -Rf /,它将会删除你硬盘上所有的东西,并且不会给你任何提示,所以,尽量在使用命令时加上-i 的参数,以让系统在执行前进行一次确认,防止你干一些蠢事。如 果你觉得每次都要输入-i 太麻烦,你可以执行以下的命令,让-i 成为默认参数:
    alias rm='rm -i'

  • ps:查询当前进程;
    这条命令会列出你所启动的所有进程:

ps -a        #可以例出系统当前运行的所有进程,包括由其他用户启动的进程;
ps auxww    #是一条相当人性化的命令,它会例出除一些很特殊进程以外的所有进程,并会以一个高可读的形式显示结果,每一个进程都会有较为详细的解释;

控制流程

  1. 输入/输出:
  • input 用来读取你通过键盘(或其他标准输入设备)输入的信息
  • output 用于在屏幕(或其他标准输出设备)上输出你指定的输出内容.另外还有一些标准的出错提示也是通过这个命令来实现的。通常在遇到操作错误时,系统会自动调用这个命令来输出标准错误提示;
    我们能重定向命令中产生的输入和输出流的位置。
  1. 重定向:如果你想把命令产生的输出流指向一个文件而不是(默认的)终端,你可以使用如下的语句:
chy@chyhome-PC:~$ ls >file4.txt
chy@chyhome-PC:~$ cat file4.txt
file1.txt  file2.txt file3.txt

以上例子将创建文件 file4.txt 如果 file4.txt 不存在的话。注意:如果 file4.txt 已经存在,那么上面的命令将复盖文件的内容。如果你想将内容添加到已存在的文件内容的最后,那你可以用下面这个语句:
command >> filename
示例:

chy@chyhome-PC:~$ ls >> file4.txt
chy@chyhome-PC:~$ cat file4.txt
file1.txt  file2.txt file3.txt
file1.txt  file2.txt file3.txt file4.txt

在这个例子中,你会发现原有的文件中添加了新的内容。接下来我们会见到另一种重定向方式:我们将把一个文件的内容作为将要执行的命令的输入。以下是这个语句:

command < filename

示例:

chy@chyhome-PC:~$ cat > file5.txt
a3.txt
a2.txt
file2.txt
file1.txt
<Ctrl-D>  # 这表示敲入Ctrl+D键
chy@chyhome-PC:~$ sort < file5.txt
a2.txt
a3.txt
file1.txt
file2.txt
  1. 管道
    Linux 的强大之处在于它能把几个简单的命令联合成为复杂的功能,通过键盘上的管道符号’|’ 完成。现在,我们来排序上面的"grep"命令:
    grep -i command < myfile | sort > result.text
    搜索 myfile 中的命令,将输出分类并写入分类文件到 result.text 。 有时候用 ls 列出很多命令的时候很不方便 这时“|”就充分利用到了 ls -l | less 慢慢看吧
  2. 后台进程
    CLI 不是系统的串行接口。您可以在执行其他命令时给出系统命令。要启动一个进程到后台,追加一个“&”到命令后面
sleep 60 &
ls

睡眠命令在后台运行,您依然可以与计算机交互。除了不同步启动命令以外,最好把 ‘&’ 理解成 ‘;’。

如果您有一个命令将占用很多时间,您想把它放入后台运行,也很简单。只要在命令运行时按下 ctrl-z,它就会停止。然后键入 bg 使其转入后台。fg 命令可使其转回前台。

sleep 60
<ctrl-z> # 这表示敲入Ctrl+Z键
bg
fg
  1. 获取软件包
    (1) 命令行获取软件包

Ubuntu 下可以使用 apt-get 命令,apt-get 是一条 Linux 命令行命令,适用于 deb 包管理式的操作系统,主要用于自动从互联网软件库中搜索、安装、升级以及卸载软件或者操作系统。一般需要 root 执行权限,所以一般跟随 sudo 命令,如:
sudo apt-get install gcc [ENTER]
常见的以及常用的 apt 命令有: apt-get install 下载 以及所依赖的软件包,同时进行软件包的安装或者升级。 apt-get remove 移除 以及所有依赖的软件包。 apt-cache search 搜索满足 的软件包。 apt-cache show/showpkg 显示软件包 的完整描述。 例如:

chy@chyhome-PC:~$apt-cache search gcc
gcc-4.8 - The GNU C compiler
gcc-4.8-base - The GNU Compiler Collection (base package)
gcc-4.8-doc - Documentation for the GNU compilers (gcc, gobjc, g++)
gcc-4.8-multilib - The GNU C compiler (multilib files)
gcc-4.8-source - Source of the GNU Compiler Collection
gcc-4.8-locales - The GNU C compiler (native language support files)
chy@chyhome-PC:~$

(2) 图形界面软件包获取 新立得软件包管理器,是 Ubuntu 下面管理软件包得图形界面程序,相当于命令行中得 apt 命令。进入方法可以是

菜单栏 > 系统管理 > 新立得软件包管理器
(System > Administration > Synaptic Package Manager)

使用更新管理器可以通过标记选择适当的软件包进行更新操作。

(3) 配置升级源

Ubuntu 的软件包获取依赖升级源,可以通过修改 “/etc/apt/sources.list” 文件来修改升级源(需要 root 权限);或者修改新立得软件包管理器中 “设置 > 软件库”。

AT&T汇编基本语法

    * 寄存器命名原则
        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)
 32位的机器下,地址的计算式:imm32 + basepointer + nidexpointer * indexscale

Examples:

  * 直接寻址
        AT&T:  foo                         Intel: [foo]
        boo是一个全局变量。注意加上$是表示地址引用,不加是表示值引用。对于局部变量,可以通过堆栈指针引用。

* 寄存器间接寻址
        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内联:

  • 基本内联汇编语句:
    格式:asm("statements")
    "asm"与“asm”的含义完全一样。如果有多行汇编,则每一行都要加上“\n\t”。其中的“\n”是换行符,“\t”是tab符,在每条指令结束时加上这两个符号,是为了让gcc把内联汇编代码翻译成一般的汇编代码时能够保证换行和留有一定的空格。对于基本asm语句,GCC编译出来的汇编代码就是双引号里面的内容。
    例如:
        asm( "pushl %eax\n\t"
          "movl $0,%eax\n\t"
          "popl %eax"
     );

实际上 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 内联汇编语法。

  • GCC扩展内联汇编
    例子:
    使用 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” 指令被删除、移动或组合,在执行代码时,如果不希望汇编语句被 gcc 优化而改变位置,就需要在 asm 符号后添加 volatile 关键词:asm volatile(…);或者更详细地说明为:asm volatile(…);然后就是小括弧,括弧中的内容是具体的内联汇编指令代码。 “” 为汇编指令部分,例如,“movl %%cr0,%0\n\t”。数字前加前缀 “%“,如%1,%2 等表示使用寄存器的样板操作数。可以使用的操作数总数取决于具体 CPU 中通用寄存器的数 量,如 Intel 可以有 8 个。指令中有几个操作数,就说明有几个变量需要与寄存器结合,由 gcc 在编译时根据后面输出部分和输入部分的约束条件进行相应的处理。由于这些样板操作数的前缀使用了”%“,因此,在用到具体的寄存器时就在前面加两个“%”,如%%cr0。输出部分(output operand list),用以规定对输出变量(目标操作数)如何与寄存器结合的约束(constraint),输出部分可以有多个约束,互相以逗号分开。每个约束以“=”开头,接着用一个字母来表示操作数的类型,然后是关于变量结合的约束。例如,上例中:

:"=r" (__dummy)
“= r”表示相应的目标操作数(指令部分的%0)可以使用任何一个通用寄存器,并且变量__dummy 存放在这个寄存器中,但如果是:

:“=m”(__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):输入部分与输出部分相似,但没有“=”。如果输入部分一个操作数所要求使用的寄存器,与前面输出部分某个约束所要求的是同一个寄存器,那就把对应操作数的编号(如“1”,“2”等)放在约束条件中。在后面的例子中,可看到这种情况。修改部分(clobber list,也称 乱码列表):这部分常常以“memory”为约束条件,以表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变。乱码列表通知编译器,有些寄存器或内存因内联汇编块造成乱码,可隐式地破坏了条件寄存器的某些位(字段)。 注意,指令部分为必选项,而输入部分、输出部分及修改部分为可选项,当输入部分存在,而输出部分不存在时,冒号“:”要保留,当“memory”存在时,三个冒号都要保留,例如

#define __cli() __asm__ __volatile__("cli": : :"memory")

下面是一个例子:

int count=1;
int value=1;
int buf[10];
void main()
{
    asm(
        "cld \n\t"
        "rep \n\t"
        "stosl"
    :
    : "c" (count), "a" (value) , "D" (buf)
    );
}

得到的主要汇编代码为:

movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP

cld,rep,stos 这几条语句的功能是向 buf 中写上 count 个 value 值。冒号后的语句指明输入,输出和被改变的寄存器。通过冒号以后的语句,编译器就知道你的指令需要和改变哪些寄存器,从而可以优化寄存器的分配。其中符号"c"(count)指示要把 count 的值放入 ecx 寄存器。类似的还有:

a eax
b ebx
c ecx
d edx
S esi
D edi
I 常数值,(0 - 31)
q,r 动态分配的寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器(use long longs)
也可以让 gcc 自己选择合适的寄存器。如下面的例子:

asm("leal (%1,%1,4),%0"
    : "=r" (x)
    : "0" (x)
);

这段代码到的主要汇编代码为:

movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x

几点说明:

[1] 使用 q 指示编译器从 eax, ebx, ecx, edx 分配寄存器。 使用 r 指示编译器从 eax, ebx, ecx, edx, esi, edi 分配寄存器。
[2] 不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们。
[3] "="是标示输出寄存器,必须这样用。
[4] 数字%n 的用法:数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求的寄存器.如果要重用"r"或"q"请求的寄存器的话,就可以使用它们。
[5] 如果强制使用固定的寄存器的话,如不用%1,而用 ebx,则:
asm(“leal (%%ebx,%%ebx,4),%0”
: “=r” (x)
: “0” (x)
);

注意要使用两个%,因为一个%的语法已经被%n 用掉了。

使用硬件模拟器 QEMU

运行参数
如果 qemu 使用的是默认 /usr/local/bin 安装路径,则在命令行中可以直接使用 qemu 命令运行程序。qemu 运行可以有多参数,格式如:

qemu [options] [disk_image]
其中 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 中 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 支持的关于系统状态信息的操作。

注意:qemu 默认有 ‘singlestep arg’ 命令(arg 为 参数),该命令为设置单步标志命令。例如:‘singlestep off’ 运行结果为禁止单步,‘singlestep on’ 结果为允许单步。在允许单步条件下,使用 cont 命令进行单步操作。如:

(qemu) xp /3i $pc
0xfffffff0: ljmp $0xf000, $0xe05b
0xfffffff5: xor    %bh, (%bx, %si)
0xfffffff7: das
(qemu) singlestep on
(qemu) cont
0x000fe05b: xor %ax, %ax

step 命令为单步命令,即 qemu 执行一步,能够跳过 breakpoint 断点执行。如果此时使用 cont 命令,则 qemu 运行改为连续执行。

log 命令能够保存 qemu 模拟过程产生的信息(与 qemu 运行参数 `-d’ 相同),具体参数可以参考命令帮助。产生的日志信息保存在 “/tmp/qemu.log” 中,例如使用 'log in_asm’命令以后,运行过程产生的的 qemu.log 文件为:

1  ----------------
2  IN:
3  0xfffffff0:  ljmp   $0xf000,$0xe05b
4
5  ----------------
6  IN:
7  0x000fe05b:  xor    %ax,%ax
8  0x000fe05d:  out    %al,$0xd
9  0x000fe05f:  out    %al,$0xda
10 0x000fe061:  mov    $0xc0,%al
11 0x000fe063:  out    %al,$0xd6
12 0x000fe065:  mov    $0x0,%al
13 0x000fe067:  out    %al,$0xd4

基于 qemu 内建模式调试 ucore
调试举例:调试 lab1,跟踪 bootmain 函数:

(1) 运行 qemu -S -hda ucore.img -monitor stdio

(2) 查看 bootblock.asm 得到 bootmain 函数地址为 0x7d60,并插入断点。

(3) 使用命令 c 连续执行到断点。

(4) 使用 xp 命令进行反汇编。

(5) 使用 s 命令进行单步执行。 运行结果如下:

chy@laptop: ~/lab1$ qemu -S -hda ucore.img -monitor stdio
(qemu) b 0x7d60
insert breakpoint 0x7d60 success!
(qemu) c
    working …
(qemu)
    break:
        0x00007d60:  push   %ebp
(qemu) xp /10i $pc
    0x00007d60:  push   %ebp
    0x00007d61:  mov    %esp,%ebp
    0x00007d63:  push   %esi
    0x00007d64:  push   %ebx
    0x00007d65:  sub   $0x4,%esp
    0x00007d68:  mov    0x7da8,%esi
    0x00007d6e:  mov    $0x0,%ebx
    0x00007d73:  movsbl (%esi,%ebx,1),%eax
    0x00007d77:  mov    %eax,(%esp,1)
    0x00007d7a:  call   0x7c6c
(qemu) step
    0x00007d61:  mov    %esp,%ebp
(qemu) step
    0x00007d63:  push   %esi

结合gdb 和 qemu 源码级调试ucore

编译可调试的目标文件

为了使得编译出来的代码是能够被 gdb 这样的调试器调试,我们需要在使用 gcc 编译源文件的时候添加参数:"-g"。这样编译出来的目标文件中才会包含可以用于调试器进行调试的相关符号信息。

ucore代码编译

(1) 编译过程:在解压缩后的 ucore 源码包中使用 make 命令即可。例如 lab1 中:

chy@laptop: ~/lab1$ make
在 lab1 目录下的 bin 目录中,生成一系列的目标文件:

  • ucore.img:被 qemu 访问的虚拟硬盘文件
  • kernel: ELF 格式的 toy ucore kernel 执行文,被嵌入到了 ucore.img 中
  • bootblock: 虚拟的硬盘主引导扇区(512 字节),包含了 bootloader 执行代码,被嵌入到了 ucore.img 中
  • sign:外部执行程序,用来生成虚拟的硬盘主引导扇区
    还生成了其他很多文件,这里就不一一列举了。

(2) 保存修改:

使用 diff 命令对修改后的 ucore 代码和 ucore 源码进行比较,比较之前建议使用 make clean 命令清除不必要文件。(如果有 ctags 文件,需要手工清除。)

使用远程调试

为了与 qemu 配合进行源代码级别的调试,需要先让 qemu 进入等待 gdb 调试器的接入并且还不能让 qemu 中的 CPU 执行,因此启动 qemu 的时候,我们需要使用参数-S –s 这两个参数来做到这一点。在使用了前面提到的参数启动 qemu 之后,qemu 中的 CPU 并不会马上开始执行,这时我们启动 gdb,然后在 gdb 命令行界面下,使用下面的命令连接到 qemu:

(gdb) target remote 127.0.0.1:1234
然后输入 c(也就是 continue)命令之后,qemu 会继续执行下去,但是 gdb 由于不知道任何符号信息,并且也没有下断点,是不能进行源码级的调试的。为了让 gdb 获知符号信息,需要指定调试目标文件,gdb 中使用 file 命令:

(gdb) file ./bin/kernel
之后 gdb 就会载入这个文件中的符号信息了。

通过 gdb 可以对 ucore 代码进行调试,以 lab1 中调试 memset 函数为例:

(1) 运行 qemu -S -s -hda ./bin/ucore.img -monitor stdio

(2) 运行 gdb 并与 qemu 进行连接

(3) 设置断点并执行

(4) qemu 单步调试。

运行过程以及结果如下:

窗口一窗口二
chy@laptop: ~/lab1$ qemu -S -s -hda ./bin/ucore.imgchy@laptop: ~/lab1$ gdb ./bin/kernel
(gdb) targetremote:1234Remote debugging using :12340x0000fff0 in ?? ()
(gdb) continueContinuing.Breakpoint 1, memset (s=0xc029b000, c=0x0, n=0x1000) at libs/string.c:271271memset(void *s, char c, size_t n) {
(gdb)
 类似资料: