lab0_准备知识

准备知识

  1. gdb 可以通过file指定加载符号链接, linux下有.pdb文件吗? 实际需要编一波,印象中qt出来的dll没有pdb文件
(gdb) file [FilePath]
  1. 用gdb查看源代码可以用list命令,但是这个不够灵活。可以使用"layout src"命令,或者按Ctrl-X再按A,就会出现一个窗口可以查看源代码。也可以用使用-tui参数,这样进入gdb里面后就能直接打开代码查看窗口。其他代码窗口相关命令:

    指令解释
    info win显示窗口的大小
    layout next切换到下一个布局模式
    layout prev切换到上一个布局模式
    layout src只显示源代码
    layout asm只显示汇编代码
    layout split显示源代码和汇编代码
    layout regs增加寄存器内容显示
    focus cmd/src/asm/regs/next/prev切换当前窗口
    refresh刷新所有窗口
    tui reg next显示下一组寄存器
    tui reg system显示系统寄存器
    update更新源代码窗口和当前执行点
    winheight name +/- line调整name窗口的高度
    tabset nchar设置tab为nchar个字符
  2. gdb调试动态链接库
    在上面小节,我们提到为了能够让gdb识别变量的符号,我们必须给gdb载入符号表等信息。在进行gdb本地应用程序调试的时候,因为在指定了执行文件时就已经加载了文件中包含的调试信息,因此不用再使用gdb命令专门加载了。但是在使用qemu进行远程调试的时候,我们必须手动加载符号表,也就是在gdb中用file命令。

    这样加载调试信息都是按照elf文件中制定的虚拟地址进行加载的,这在静态连接的代码中没有任何问题。但是在调试含有动态链接库的代码时,动态链接库的ELF执行文件头中指定的加载虚拟地址都是0,这个地址实际上是不正确的。从操作系统角度来看,用户态的动态链接库的加载地址都是由操作系统动态分配的,没有一个固定值。然后操作系统再把动态链接库加载到这个地址,并由用户态的库链接器(linker)把动态链接库中的地址信息重新设置,自此动态链接库才可正常运行。
    由于分配地址的动态性,gdb并不知道这个分配的地址是多少,因此当我们在对这样动态链接的代码进行调试的时候,需要手动要求gdb将调试信息加载到指定地址。
    下面,我们要求gdb将linker加载到0x6fee6180这个地址上:
    (gdb) add-symbol-file android_test/system/bin/linker 0x6fee6180
    这样的命令默认是将代码段(.data)段的调试信息加载到0x6fee6180上,当然,你也可以通过“-s”这个参数来指定,比如: (gdb) add-symbol-file android_test/system/bin/linker –s .text 0x6fee6180
    这样,在执行到linker中代码时gdb就能够显示出正确的代码和调试信息出来。
    这个方法在操作系统中调试动态链接器时特别有用。

  3. 使用gdb配置文件
    在上面可以看到,为了进行源码级调试,需要输入较多的东西,很麻烦。为了方便,可以将这些命令存在脚本中,并让gdb在启动的时候自动载入。
    以lab1为例,在lab1/tools目录下,执行完make后,我们可以创建文件gdbinit,并输入下面的内容:

    target remote 127.0.0.1:1234
    file bin/kernel
    

    为了让gdb在启动时执行这些命令,使用下面的命令启动gdb:

    $ gdb -x tools/gdbinit
    

    如果觉得这个命令太长,可以将这个命令存入一个文件中,当作脚本来执行。
    另外,如果直接使用上面的命令,那么得到的界面是一个纯命令行的界面,不够直观,就像下图这样:

    如果想获得上面右图那样的效果,只需要再加上参数-tui就行了,比如:

    gdb -tui -x tools/gdbinit
    
  4. 设定调试目标架构
    在调试的时候,我们也许需要调试不是i386保护模式的代码,比如8086实模式的代码,我们需要设定当前使用的架构:

    (gdb) set arch i8086 
    

处理器硬件

intel80386运行模式

一般CPU只有一种运行模式,能够支持多个程序在各自独立的内存空间中并发执行,且有用户特权级和内核特权级的区分,让一般应用不能破坏操作系统内核和执行特权指令。80386处理器有四种运行模式:实模式、保护模式、SMM模式和虚拟8086模式。这里对涉及ucore的实模式、保护模式做一个简要介绍

实模式:这是个人计算机早期的8086处理器采用的一种简单运行模式,当时微软的MS-DOS操作系统主要就是运行在8086的实模式下。80386加电启动后处于实模式运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的 (了解下即可)

保护模式:保护模式的一个主要目标是确保应用程序无法对操作系统进行破坏。实际上,80386就是通过在实模式下初始化控制寄存器(如GDTR,LDTR,IDTR与TR等管理寄存器)以及页表,然后再通过设置CR0寄存器使其中的保护模式使能位置位,从而进入到80386的保护模式。当80386工作在保护模式下的时候,其所有的32根地址线都可供寻址,物理寻址空间高达4GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。保护模式下80386支持多任务,还支持优先级机制,不同的程序可以运行在不同的特权级上。特权级一共分0~3四个级别,操作系统运行在最高的特权级0上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务 (主要实验环境)

intel80386内存架构

物理地址:是CPU通过总线访问物理内存用到的物理地址
逻辑地址:是我们写程序是所用的到的地址,也称为虚拟地址;

int boo=1;
int *foo=&a;

foo存储着boo的地址,这就是逻辑地址;

三个地址空间概念:物理地址空间、线性地址空间和逻辑地址空间

物理地址空间: 是处理器提交到总线上用于访问计算机系统中的内存和外设的最终地址.
线性地址空间:是80386处理器通过段(Segment)机制控制下的形成的地址空间;在操作系统的管理下,每个运行的应用程序有相对独立的一个或多个内存空间段,每个段有各自的起始地址和长度属性,大小不固定,这样可让多个运行的应用程序之间相互隔离,实现对地址空间的保护。

在操作系统完成对80386处理器段机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立全局描述符表,完成虚拟地址与线性地址的映射关系)后,80386处理器的段管理功能单元负责把逻辑地址(虚拟地址)转换成线性地址,在没有下面介绍的页机制启动的情况下,这个线性地址就是物理地址。

相对而言,段机制对大量应用程序分散地使用大内存的支持能力较弱。所以Intel公司又加入了页机制,每个页的大小是固定的(一般为4KB),也可完成对内存单元的安全保护,隔离,且可有效支持大量应用程序分散地使用大内存的情况。
在操作系统完成对80386处理器页机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立页表,完成虚拟地址与线性地址的映射关系)后,应用程序看到的逻辑地址先被处理器中的段管理功能单元转换为线性地址,然后再通过80386处理器中的页管理功能单元把线性地址转换成物理地址。

页机制和段机制有一定程度的功能重复,但Intel公司为了向下兼容等目标,使得这两者一直共存。

上述三种地址的关系如下:

  • 分段机制启动、分页机制未启动:逻辑地址—>段机制处理—>线性地址=物理地址
  • 分段机制和分页机制都启动:逻辑地址—>段机制处理—>线性地址—>页机制处理—>物理地址
Intel 80386寄存器

这里假定读者对80386 CPU有一定的了解,所以只作简单介绍。80386的寄存器可以分为8组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,它们的宽度都是32位。一般程序员看到的寄存器包括通用寄存器,段寄存器,指令指针寄存器,标志寄存器。

General Register(通用寄存器):EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP这些寄存器的低16位就是8086的 AX/BX/CX/DX/SI/DI/SP/BP,对于AX,BX,CX,DX这四个寄存器来讲,可以单独存取它们的高8位和低8位 (AH,AL,BH,BL,CH,CL,DH,DL)。它们的含义如下:

EAX:累加器
EBX:基址寄存器
ECX:计数器
EDX:数据寄存器
ESI:源地址指针寄存器
EDI:目的地址指针寄存器
EBP:基址指针寄存器
ESP:堆栈指针寄存器

记是记不住的,后面用多了,才能熟能生巧

面向对象的编程方法

用C来写面向对象程序,emmm,amazing, 不是很明白这个结构体就实现面向对象编程了???
答:后面了解到,还真是,这样的循环链表实现,是通用的,不限制具体的数据类型,感觉还比c++的类或者泛型还厉害;至少这是从头开始敲的,不知道c++的泛型底层是怎么实现的,哈哈哈,有空再研究一波~

uCore的面向对象编程方法,目前主要是采用了类似C++的接口(interface)概念,即是让实现细节不同的某类内核子系统(比如物理内存分配器、调度器,文件系统等)有共同的操作方式,这样虽然内存子系统的实现千差万别,但它的访问接口是不变的。这样不同的内核子系统之间就可以灵活组合在一起,实现风格各异,功能不同的操作系统。接口在 C 语言中,表现为一组函数指针的集合。放在 C++ 中,即为虚表(虚函数表???)。接口设计的难点是如果找出各种内核子系统的共性访问/操作模式,从而可以根据访问模式提取出函数指针列表。

比如对于uCore内核中的物理内存管理子系统,首先通过分析内核中其他子系统可能对物理内存管理子系统,明确物理内存管理子系统的访问/操作模式,然后我们定义了pmm_manager数据结构(位于lab2/kern/mm/pmm.h)如下:

// pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_manager
// only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used
// by ucore to manage the total physical memory space.

struct pmm_manager {
    // XXX_pmm_manager's name
    const char *name;  
    // initialize internal description&management data structure
    // (free block list, number of free block) of XXX_pmm_manager 
    void (*init)(void); 
    // setup description&management data structcure according to
    // the initial free physical memory space 
    void (*init_memmap)(struct Page *base, size_t n); 
    // allocate >=n pages, depend on the allocation algorithm 
    struct Page *(*alloc_pages)(size_t n);  
    // free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
    void (*free_pages)(struct Page *base, size_t n);   
    // return the number of free pages 
    size_t (*nr_free_pages)(void);                     
    // check the correctness of XXX_pmm_manager
    void (*check)(void);                               
};

这样基于此数据结构,我们可以实现不同连续内存分配算法的物理内存管理子系统,而这些物理内存管理子系统需要编写算法,把算法实现在此结构中定义的init(初始化)、init_memmap(分析空闲物理内存并初始化管理)、alloc_pages(分配物理页)、free_pages(释放物理页)函数指针所对应的函数中。而其他内存子系统需要与物理内存管理子系统交互时,只需调用特定物理内存管理子系统所采用的pmm_manager数据结构变量中的函数指针即可;

双向循环链表

uCore的双向链表结构定义为:

struct list_entry {
    struct list_entry *prev, *next;
};

空闲块链表的头指针定义(位于lab2/kern/mm/memlayout.h中)为:

/* 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;

而每一个空闲块链表节点定义(位于lab2/kern/mm/memlayout)为:
/* *
 * 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
};

这样以free_area_t结构的数据为双向循环链表的链表头指针,以Page结构的数据为双向循环链表的链表节点,就可以形成一个完整的双向循环链表,如下图所示:

在这里插入图片描述
传统数据结构的 循环链表有头指针这么一说么?
答:为了使空链表和非空链表处理一致,我们通常设一个头结点,但是,并不是说循环链表一定要有头结点,一般说循环链表有单向和双向之分,单向循环链表就是单链表的尾结点的next指针指向头结点即可,这时候形成的链表是没有头结点的循环链表;双向链表结点是有prev指针和next指针;

ucore的这个循环链表,更是一个带头结点的双向循环链表,头指针的数据结构和结点指针的数据结构不一致,在于数据成员上

有关这些链表操作函数的定义如下。

(1) 初始化
uCore只定义了链表节点,并没有专门定义链表头,那么一个双向循环链表是如何建立起来的呢?让我们来看看list_init这个内联函数(inline funciton):

static inline void
list_init(list_entry_t *elm) {
    elm->prev = elm->next = elm;
}

参看文件default_pmm.c的函数default_init,当我们声明一个名为free_area.free_list的链表头时,接着调用list_init(&(free_area.free_list)),它的next、prev指针都初始化为指向自己,这样,我们就有了一个表示空闲内存块链的空链表。而且我们可以用头指针的next是否指向自己来判断此链表是否为空,而这就是内联函数list_empty的实现。

(2) 插入
对链表的插入有两种操作,即在表头插入(list_add_after)或在表尾插入(list_add_before)。因为双向循环链表的链表头的next、prev分别指向链表中的第一个和最后一个节点,所以,list_add_after和list_add_before的实现区别并不大,实际上uCore分别用list_add(elm, listelm, listelm->next)和list_add(elm, listelm->prev, listelm)来实现在表头插入和在表尾插入。而__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;
}

(3) 删除
当需要删除空闲块链表中的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的prev、next指针分别自身,即将节点置为空链状态。这可以通过list_del_init函数来完成。

(4) 访问链表节点所在的宿主数据结构
通过上面的描述可知,list_entry_t通用双向循环链表中仅保存了某特定数据结构中链表节点成员变量的地址,那么如何通过这个链表节点成员变量访问到它的所有者(即某特定数据结构的变量)呢?Linux为此提供了针对数据结构XXX的le2XXX(le, member)的宏,其中le,即list entry的简称,是指向数据结构XXX中list_entry_t成员变量的指针,也就是存储在双向循环链表中的节点地址值, member则是XXX数据类型中包含的链表节点的成员变量。例如,我们要遍历访问空闲块链表中所有节点所在的基于Page数据结构的变量,则可以采用如下编程方式(基于lab2/kern/mm/default_pmm.c):

//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数据结构的变量
    ……
}

没懂这个page_link是什么意思,也就是后续的member?

page_link 是page结构的一个成员变量;

le2page宏(定义位于lab2/kern/mm/memlayout.h)的使用相当简单:

// convert list entry to page
#define le2page(le, member)                 \
to_struct((le), struct Page, member)

而相比之下,它的实现用到的to_struct宏和offsetof宏(定义位于lab2/libs/defs.h)则有一些难懂:

/* Return the offset of 'member' relative to the beginning of a struct type */
#define offsetof(type, member)                                      \
((size_t)(&((type *)0)->member))

/* *
 * to_struct - get the struct from a ptr
 * @ptr:    a struct pointer of member
 * @type:   the type of the struct this is embedded in
 * @member: the name of the member within the struct
 * */
#define to_struct(ptr, type, member)                               \
((type *)((char *)(ptr) - offsetof(type, member)))

这里采用了一个利用gcc编译器技术的技巧,即先求得数据结构的成员变量在本宿主数据结构中的偏移量,然后根据成员变量的地址反过来得出属主数据结构的变量的地址。
我们首先来看offsetof宏,size_t最终定义与CPU体系结构相关,本实验都采用Intel X86-32 CPU,故szie_t等价于 unsigned int。 (type )0)->member的设计含义是什么?其实这是为了求得数据结构的成员变量在本宿主数据结构中的偏移量。为了达到这个目标,首先将0地址强制"转换"为type数据结构(比如struct Page)的指针,再访问到type数据结构中的member成员(比如page_link)的地址,即是type数据结构中member成员相对于数据结构变量的偏移量。 在offsetof宏中,这个member成员的地址(即“&((type *)0)->member)”)实际上就是type数据结构中member成员相对于数据结构变量的偏移量。对于给定一个结构,offsetof(type,member)是一个常量,to_struct宏正是利用这个不变的偏移量来求得链表数据项的变量地址。接下来再分析一下to_struct宏,可以发现 to_struct宏中用到的ptr变量是链表节点的地址,把它减去offsetof宏所获得的数据结构内偏移量*,即就得到了包含链表节点的属主数据结构的变量的地址

注意下结构体变量本身地址和它的成员变量地址的关系
目前理解是: 本身地址是低地址 变量地址依次增高;

这边拿到的是变量地址,要去找结构体的本身地址, 知道的是结构体和变量地址固定的地址偏移量; 高地址减低地址就得到变量地址;这就要求((size_t)(&((type *)0)->member))得到的值一定是正的,但是这个操作不就相当于访问空指针嘛,笑哭?

出于很好奇,我还是准备验证下一下:

网上搜了下,这是gcc编译器的优化; 网上说这个偏移量是在编译时确定的,并不是在运行时。

Gcc和vs2015验证了下,都可以正常编译,运行, gcc的结果是8. vs2015的结果是4, 跟规定的字节对齐方式有关;

#include <stdio.h>

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

struct link_entry {
    struct link_entry *prev,*next;
};

struct Page{
    int data;
    struct link_entry node_link;
};

int main()
{
    size_t a = offsetof(struct Page, node_link);
    printf("%ld\n", a);
    return 0;
}

本身这个offsetof宏也是存在的,C++定义于头文件 , c 定义于头文件 <stddef.h> 看头文件基本上就能看出来这个宏定义的基本实现版本,有三个,#define offsetof(s,m) ((size_t)&(((s*)0)->m)) 和我们的定义一致,emmm,看到这里基本可以释怀了,不过还是有点惊讶,竟然没有空指针访问,哈哈哈哈,以后这个宏平时也可以用用看,nice~

//  <stddef.h>  vs2015的头文件
// Define offsetof macro
#if defined(_MSC_VER) && !defined(_CRT_USE_BUILTIN_OFFSETOF)
    #ifdef __cplusplus
        #define offsetof(s,m) ((size_t)&reinterpret_cast<char const volatile&>((((s*)0)->m)))
    #else
        #define offsetof(s,m) ((size_t)&(((s*)0)->m))
    #endif
#else
    #define offsetof(s,m) __builtin_offsetof(s,m)
#endif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值