QNX startup程序分析

QNX相关历史文章:

这篇文章主要描述QNX的startup程序功能及组成,分析了system page结构,以及该结构中跟硬件相关性较大的hwinfo段与callout段。

1. startup介绍

在一个可启动的QNX镜像中,startup是第一个启动程序,startup程序的作用包括:

  • 初始化硬件
    完成基础的硬件初始化,具体需要做多少初始化工作,取决于IPL loader中做了多少。有时只需要做很少的初始化:比如MMU、定时器、中断控制器。
  • 初始化系统页system page
    关于系统的信息存放在一个叫做系统页(system page)的数据结构中,包括处理器类型、总线类型,可用的系统RAM位置和大小、硬件配置、缓存、内存映射和地址空间、定时器参数等,存放该结构的页面区域是内存的专用区域。内核和应用软件都可以以只读的形式来访问这些信息。
  • 初始化callout
    系统页system page结构中还包含了内核callouts字段,内核callout用于提供一些板级相关的代码,比如内核调试、系统定时器、缓存控制、中断处理、系统重启等,最终被QNX内核调用。这些代码都由startup程序来提供,由内核来负责回调进而操作硬件。这样的实现可以让QNX系统与硬件进行解耦合,具备更好的移植性。
  • 加载并将控制权转交给镜像中的下一个程序
    在这个阶段所做的工作包括初始化MMU,创建处理分页、进程和异常的结构,使能中断等,之后就可以跳转到内核运行了。

2. startup程序结构

代码位于{BSP_ROOT_DIR}/src/hardware/startup目录中,以R-Car为例:


从图中可以看出,boards目录下放置的rcar_gens,表明是瑞萨R-Car的第三代SoC,在该目录下的rcar_h3和rcar_m3分别对应两个不同的系列,黄色箭头所指的_start.S为程序的总体入口。

从_start.S进去,会涉及到ARM V8处理器的一系列初始化和设置,这部分是通用的,最终会调到main()函数,而这个main()函数,正是R-Car的startup的一部分。main()函数位于上图中的boards/rcar_gen3/目录下。

每个startup程序,都会包含一个main()函数,main()函数的伪代码如下:

Global variables
 
main()
{
    Call add_callout_array (note 1)
 
    Argument parsing (note 2)
 
    Call init_raminfo (note 3)
 
    Remove ram used by modules in the image
 
    if (virtual)
        Call init_mmu (note 4)
 
    Call init_intrinfo (note 5)
 
    Call init_qtime (note 6)
 
    Call init_cacheattr (note 7)
 
    Call init_cpuinfo (note 8)
 
    Set hardware machine name
 
    Call init_system_private (note 9)
 
    Call print_syspage (note 10)
}

上述代码中对应的注释note如下:

  • note 1
    将callout添加到system page结构中,上文中提到过callout本质上是回调函数,这个过程相当于把信息注册进系统页这个结构中;
  • note 2
    参数解析,对传入的ASCII字符进行处理,通过'switch case'来选择对应的项,操作包括:Reboot、输出通道的选择(kprintf或stdout)、CPU频率/时钟频率/定时器频率、Reserve内存、确定在SMP系统中CPU个数等;
  • note 3
    确定可用系统RAM的位置和大小,并在system page结构中初始化asinfo结构,如果已经知道了RAM的确切数量和位置,可以使用一个自定义的函数(在这个函数中可以使用add_ram来进行硬编码)来代替这个函数;
  • note 4
    设置MMU,通过设置页表,来完成物理地址到虚拟地址的转换;
  • note 5
    设置中断系统的相关信息,比如中断向量表相关;
  • note 6
    初始化system page中的qtime结构,qtime结构包含关于系统上的基准时间信息,以及其他与时间相关的信息;
  • note 7
    初始化缓存相关内容,包含片内和片外的缓存气筒,对所有平台,这部分都只是一个占位符,没有实现;
  • note 8
    初始化CPU相关信息,比如CPU类型、速度、功能、性能和缓存大小等;
  • note 9
    这个模块在所有平台上,都不需要修改,完成的工作包括:找到所有需要启动的引导镜像,并用这些信息填充结构;告诉内核镜像文件系统的位置;为system page结构分配实际的存储空间等;
  • note 10
    打印system page结构中的所有成员内容,其中全局变量debug_level用于确定输出的内容,debug_level至少为2才能打印任何内容,debug_level为3将打印子结构中的信息,system page对应的数据结构如下,这个结构中的字段,有些可通过startup库来初始化,有些则需要自己去实现代码来填充;

3. system page

从startup的代码中可以看出,在main函数中的所有处理,基本都是围绕这个system page结构来展开,完成相应段的初始化。最终在write_syspage_memory()之后,通知system page已经ready了,再调用startnext()进入下一个阶段的运行,也就是启动QNX内核。QNX内核读取这个内存区域来获取系统信息。

system page的结构,由不同的section组成,具体如下:

/*
 * contains at least the following:
 */
struct syspage_entry {
    uint16_t            size;
    uint16_t            total_size;
    uint16_t            type;
    uint16_t            num_cpu;
    syspage_entry_info  system_private;
    syspage_entry_info  asinfo;  /* address space information 结构数组,用于描述不同部分的内存映射,比如RAM、SRAM、Flash、I/O范围等,当procnto为进程地址空间管理虚拟地址时,会使用asinfo中的信息来获取可以从RAM中的何处分配内存。内存映射采用树状格式,地址范围可以有父节点,比如/memory/io/memclass/...,其中memclass可以是ram、rom、flash等 */
    syspage_entry_info  hwinfo;
    syspage_entry_info  cpuinfo;
    syspage_entry_info  cacheattr;  /* 关于片内和片外缓存系统配置的信息,该区域还包含了用于内核控制缓存操作的Callout,cacheattr结构由init_cpuinfo()和init_cacheattr()来填充,cacheattr条目组织在一个链表中,结构体中的next成员表示下一级缓存条目的索引 */
    syspage_entry_info  qtime;  /* 关于系统上显示时间的基准信息,以及其他与时间相关的信息 */
    syspage_entry_info  callout;
    syspage_entry_info  callin;
    syspage_entry_info  typed_strings;
    syspage_entry_info  strings;
    syspage_entry_info  intrinfo;   /* 中断系统信息,还包含了用于操作中断控制器硬件的内核Callout */
    syspage_entry_info  smp;
    syspage_entry_info  pminfo;
     
    union {
        struct x86_syspage_entry    x86;
        struct ppc_syspage_entry    ppc;
        struct mips_syspage_entry   mips;
        struct arm_syspage_entry    arm;
        struct sh_syspage_entry     sh;
    } un;
};

4. hwinfo

注意到上文中提到system page结构中,有一个syspage_entry_info hwinfo成员,这个结构包含了硬件平台的信息,包括总线类型、设备、中断等。

hwinfo段不是由一个单独结构或相同类型的数组组成,而是由一些标签化的结构组成,这些结构作为一个整体来描述电路板上的硬件。在hwinfo段中,有两个概念,一个是Tag,一个是Item。

Tag:
Tag结构用于描述硬件组件的特定方面的信息,Tag都是以下边这个结构开头

struct hwi_prefix {
    uint16_t        size;
    uint16_t        name;
};

目前提供了几个预定义的Tag,如下所示:

/* 它给出了寄存器的位置(不管是在I/O空间还是在内存空间),如果有多个寄存器组,则Item中可能会有多个这样的Tag */
#define HWI_TAG_NAME_location   "location"
#define HWI_TAG_ALIGN_location  (sizeof(uint64))
struct hwi_location {
    struct hwi_prefix   prefix;
    uint32_t            len;      /* 寄存器范围的长度 */
    uint64_t            base;     /* 寄存器的物理基地址 */
    uint16_t            regshift; /* Indicates the shift for each register access. */
    uint16_t            addrspace; /* 从asinfo部分开始的偏移量,以字节为单位,这个成员用于标识寄存器是内存映射还是在单独的IO地址空间 */
};
 
/* 给出了设备的中断号 */
#define HWI_TAG_NAME_irq        "irq"
#define HWI_TAG_ALIGN_irq       (sizeof(uint32))
struct hwi_irq {
    struct hwi_prefix   prefix;
    uint32_t            vector;   /* 逻辑向量中断号 */
};
 
/* 这个Tag,用于填充,保证字节能对齐 */
#define HWI_TAG_NAME_pad        "pad"
#define HWI_TAG_ALIGN_pad       (sizeof(uint32))
struct hwi_pad {
    struct hwi_prefix   prefix;
};

Item:
Item是Tag的集合,用于描述一个硬件组件的完整信息。每个Item中的第一个Tag都是以struct hwi_item开始,结构如下所示:

struct hwi_item {
    struct hwi_prefix   prefix;
    uint16_t            itemsize;    /* 到下一项Item开始的距离,以4字节为单位 */
    uint16_t            itemname;    /* 这个字段是整型,存放的是system page中strings段中对应的偏移量,用于描述Item的名称 */
    uint16_t            owner;       /* 这个字段用于将Item组织成树状结构,类似于文件系统的层次结构,存放的值是hwinfo段中对应的偏移量 */
    uint16_t            kids;        /* 当前项的子项数目 */
};

通过将Item组织,可以在hwinfo中构造一个设备树,比如通常设备层次结构为:/hw/bus/devclass/device

  • hw,硬件树的根;
  • bus,硬件所在的总线,比如pci、eisa等,对应Bus Item;
  • devclass,设备的分类,比如serial、rtc等,对应Group Item;
  • device,实际的设备,比如8250、mc146818等,对应Device Item;
    目前提供了部分的预定义的Item,如下:
/* Group Item, Item组,可以将多个Item组织在一起,它与文件系统中的目录具有相同的用途,比如/hw树中的devclass层就使用Group Item  */
#define HWI_TAG_NAME_group  "Group"
#define HWI_TAG_ALIGN_group (sizeof(uint32_t))
struct hwi_group {
    struct hwi_item     item;
};
 
/* Bus Item, 总线Item用于告诉系统硬件总线的信息 */
#define HWI_TAG_NAME_bus    "Bus"
#define HWI_TAG_ALIGN_bus   (sizeof(uint32))
struct hwi_bus {
    struct hwi_item     item;
};
/* Bus Item的名字可以是(不限于) */
#define HWI_ITEM_BUS_PCI        "pci"
#define HWI_ITEM_BUS_ISA        "isa"
#define HWI_ITEM_BUS_EISA       "eisa"
#define HWI_ITEM_BUS_MCA        "mca"
#define HWI_ITEM_BUS_PCMCIA     "pcmcia"
#define HWI_ITEM_BUS_UNKNOWN    "unknown"
 
/* Device Item, 设备Item用于告诉系统单个设备的信息 */
#define HWI_TAG_NAME_device     "Device"
#define HWI_TAG_ALIGN_device    (sizeof(uint32))
struct hwi_device {
    struct hwi_item     item;
    uint32_t            pnpid;    /* 微软分配的即插即用设备标识符,只适用于播放媒体的设备,已经弃用 */
};

上述的Item和Tag只是预定义的,用户可以创建自己需要的Item。

构建hwinfo段的接口:
针对hwinfo的Tag和Item,提供了一下相关的操作接口,如下:

/* 分配一个Tag */
void *hwi_alloc_tag(const char *name, unsigned size, unsigned align);
 
/* 分配一个Item */
void *hwi_alloc_item(const char *name, unsigned size,
                     unsigned align, const char *itemname,
                     unsigned owner);
 
/* 查到Item中的信息 */
unsigned hwi_find_item(unsigned start, ...);
 
/* 根据Tag的指针,得到在hwinfo段中的offset */
unsigned hwi_tag2off(void *);
 
/* 根据offset, 来得到Tag */
unsigned hwi_tag2off(void *);
 
/* 根据tagname,得到Tag */
unsigned hwi_find_tag(unsigned start, int curr_item, const char *tagname);
 
/* 获取给定偏移量对应的Item的下一个Item在hwinfo段中的偏移量 */
unsigned hwi_next_item( unsigned off);
 
/* 获取给定偏移量对应的Tag的下一个Tag在hwinfo段中的偏移量 */
unsigned hwi_next_tag( unsigned off, int curr_item );

要构建一个Item,有以下步骤:

  • 调用hwi_alloc_item()接口来构建一个顶层的Item,它的owner字段被设置成HWI_NULL_OFF;
  • 调用hwi_alloc_tag()接口来添加任何想要的Tag结构;
  • 调用hwi_alloc_item()来创建一个新的Item,这个Item可以是刚创建Item的第一个子项,也可以是另一个顶层的Item。

5. 内核Callout

什么是Callout?先来看几个数据结构:

struct callout_rtn {
    unsigned    *rw_size;
    void        (*patcher)(PADDR_T paddr, uintptr_t vaddr, unsigned rtn_offset, unsigned rw_offset, void *data, const struct callout_rtn *src);
    unsigned    rtn_size;
    uint8_t        rtn_code[1];
};
 
struct callout_slot {
    unsigned                    offset;
    const struct callout_rtn    *callout;
};

Callout是独立的代码片段,可以认为是一些由startup来提供的回调函数,在QNX内核中绑定调用,用于执行特定于硬件的操作。不需要静态地将这些代码链接到QNX内核中,这样做也就能将QNX内核与硬件相关的操作分离。

Callout例程通常以汇编的形式给出,Callout例程作为startup程序的一部分,在内核启动时它将被覆盖,为了避免这种情况,startup程序会将这些Callouts例程从加载的位置拷贝到一个安全的位置,所以Callouts的代码需要是位置无关(position-indepentent)的。

内核使用SoC的Application Binary Interface(ABI)来向Callout传递数据或者从Callout获取数据。当尝试为开发板编写Callout时,首先需要去熟悉板子的ABI接口文档。

5.1 内核Callout类别

startup库为内核提供了内核Callout,用于处理不同类别的任务,可以使用这些Callout当做模板来编写自己的Callout。

Kernel Debug

内核在需要打印一些内部调试信息或遇到错误时,需要用到Debug Callout,以输出调试或检测信息。

包括:

  • display_char(),从内核接收到字符,并将它输出给UART或其他设备;
  • poll_key(),传递一个字符给内核,如果字符不可用,则返回-1;
  • break_detect(),检测是否有中断;

当内核希望与串口、控制台或其他设备交互时,比如打印一些内部调试信息,会调用这些接口,其中poll_key()和break_detect()是可选的。

Clock/timer

内核使用这部分的Callout与硬件定时器交互(定时器/计数器芯片),在很多情况下,一个开发板上可能有多个计时器,可以在启动代码中选定一个Callout来使用。内核使用硬件定时器来产生周期性中断,用于软件定时器、调度、更新系统时间或其他软件时间等。

Callout包括:

  • timer_load(),负责将内核传递的值填充到硬件中,Callout会将写入硬件计时器的值写入到qtime_entry中的timer_load字段,这样内核就可以看到实际使用了哪个值;
  • timer_reload(),内核在中断开始时调用timer_reload(),如果timer_reload()返回1,则将中断视为时钟tick,当有多个中断源可以产生相同的中断时,timer_reload()返回值可以用于将时钟tick中断源与其他中断源区分开来;
  • timer_value(),返回定时器芯片内部的计数值,内核可以调用timer_value()获取下一个中断到来的时间;

内核使用这些Callout来与硬件定时器芯片交互。

Interrupt controller

中断控制器接口包括内核Callout和Stubs两部分

Callout包括:

  • mask(),mask某个中断向量;
  • unmask(),unmask某个中断向量;
  • config(),发现指定中断级别的配置;

Stubs包括:

  • interrupt_id_*(),负责将中断级别配置进CPU寄存器中,并Mask处理。Mask在处理边缘触发中断的情况下是必要的,可以防止在完成响应之前再次中断。
    需要做的工作包括:1)从某种中断状态寄存器中读取信息;2)执行一些位操作来确定中断级别;3)将中断级别值写入通用的CPU寄存器中,以便内核使用;4)在出现故障或错误断言的情况下,在GPR中写入-1,表明没有中断内核;5)操作enable和mask寄存器来屏蔽中断;

  • interrupt_eoi_*(),End of Interrupt(EOI)
    需要做的工作包括:1)告诉中断控制器,中断已经被处理;2)打开中断级别的掩码,解除屏蔽;3)在某些情况下,内核Callout会操作寄存器中的其他位,以提示中断控制器重新计算它接收到的输入。

stubs这部分代码直接集成到了内核代码中,它们的调用方式与其他的Callout调用方式不一样,不能从这两个Callout中间返回,必须执行到最后。

Cache controller

根据系统中的缓存控制器电路,可能也需要为内核提供与缓存控制器相关的Callout,用于在内核中执行某些特定功能时使部分缓存失效。

内核Callout的原型如下:

  • control(),需要传给这个Callout一些Flags标记、地址(虚拟地址或物理地址,取决于system page结构中cacheattr数组中的Flag值),需要影响的cache line数量;
    这个接口返回所影响的cache lines,返回0表明所有的cache都被invalidate了。在有的处理器架构中,缓存控制器与CPU紧密耦合,这也意味着内核不必与缓存控制器通信。

一般不太可能需要自己实现这个Callout,大多数情况下startup标准库中都提供了接口,能正常使用。

System reset

每当内核需要重新启动机器是,会调用Callout中的reboot()接口,这个可以让开发人员做一些定制化操作,比如在某些事情发生时进行重启操作。sysmgr_reboot()最终会调用到reboot()。

Power management

每当需要激活电源管理时,会去调用power(),而这个Callout是特定于CPU的。通常CPU的电源模式有以下几种:

  • Active or Running,系统正在运行应用程序,一些外围设备可能处于空闲或关闭状态;
  • Idle,系统没有运行应用程序,CPU停止,代码全部或部分驻留在内存中;
  • Standby,系统没有运行应用程序,CPU停止,代码没有驻留在内存中;
  • Shutdown,最小或零功率状态,CPU、内存和设备都关闭了电源;

5.2 Callout编写

如果startup库中的内核Callout不支持目标硬件平台,或者任何可用的特定于硬件的内核Callout也不支持目标硬件平台,那就需要自己去实现Callout了。

Callout都以汇编的形式给出来,文件的命名约定为callout_category_device.S,其中category有:cache、debug、interrupt、timer、reboot等几种,device指的是特定的设备,比如在R-Car中使用了串口,命名为callout_debug_scif.S。

在编写Callout之前,需要先查看硬件文档,以便了解内核Callout需要做什么,才能在目标硬件上完成它的任务。一般可以拷贝功能相近的Callout文件,然后在它的基础上进行修改。

编写内核Callout有几点注意的:

  • 开始和结束宏
/* 包含头文件 */
#include "callout.ah"
/* 或者如下 */
//.include "callout.ah"
 
CALLOUT_START(timer_load_8254, 0, 0)
CALLOUT_END(timer_load_8254)

CALLOUT_START宏,表示Callout的起始,有三个参数,分别代表例程名字、四字节变量的地址(该地址包含了Callout需要的读写存储量)、patcher例程的地址(0表示不需要patching)

CALLOUT_END宏,表示Callout的结束,参数与CALLOUT_START宏中的第一个参数一样。

当这个Callout被内核选中的话,CALLOUT_START和CALLOUT_END之间的代码会被拷贝到一个安全的内存区域,方便内核使用。

  • patcher例程
    如果为其编写内核Callout的设备可以出现在不同的开发板中的不同位置,则需要一个patch例程来将寄存器地址添加到内核Callout代码中。

内核Callout是startup库中的一部分,因此设计的很灵活,不会硬编码寄存器地址,而是假设寄存器地址是通过patch进来的,寄存器地址都是来自板级代码中,在板级目录的代码中可以找到。如果内核Callout只访问CPU寄存器,则不需要这个patch操作。

patcher例程的函数原型如下:

/*
 * paddr, system page开始的物理地址
 * vaddr,允许读写访问system page的虚拟地址(仅供内核使用)
 * rtn_offset,从system page开始到内核Callout代码开始的偏移量
 * rw_offset,从system page开始到读写位置的偏移量,可以由所有在CALLOUT_START宏的第二个参数中具有相同值的内核Callout共享
 * data,指向callout_register_data()注册的任意数据的指针
 * src,指向callout_rtn结构的指针,被复制到适当的位置
 */
void patcher( paddr_t paddr,
    paddr_t vaddr,
    unsigned rtn_offset,
    unsigned rw_offset,
    void *data,
    struct callout_rtn *src );

这个例程会在内核Callout被拷贝到最终位置时立马被调用。patcher例程不必使用汇编实现,但通常都是通过汇编实现,因此可以将其保存在它patching的源文件中,与CALLOUT_START/CALLOUT_END组织的代码放在一块。

  • 分配读写空间
    在某些情况下,内核Callout需要访问一些静态读写存储,特别是为了能够与其他内核Callout共享信息时。由于内核Callout代码是位置无关的,因此它不能有静态读写存储,可以在system page的末尾将少量的内存分配给内核Callout作为读写存储。使用CALLOUT_START宏的第二个参数来指定一个四字节变量的地址,该变量包含内核Callout所需的读写存储量。

Callout 示例
Callout的编写如下,以R-Car的callout_debug_scif.S为例:

/*
 * Patch interrupt callouts to access rw data.
 * The first call will also map the uart.
 *
 * Patcher routine takes the following arguments:
 *    x0 - syspage paddr
 *    x1 - vaddr of callout
 *    x2 - offset from start of syspage to start of callout routine
 *    x3 - offset from start of syspage to rw storage
 *    x4 - patch data
 *    x5 - callout_rtn
 */
 
patch_debug:
    sub        sp, sp, #16
    stp        x19, x30, [sp]
 
    add        x19, x0, x2                // x19 = address of callout routine
 
    /*
     * Map UART using patch_data parameter
     */
    mov        x0, #0x1000
    ldr        x1, [x4]
    bl        callout_io_map
 
    /*
     * Patch callout with mapped virtual address in x0
     */
    CALLOUT_PATCH    x19, w6, w7
 
    ldp        x19, x30, [sp]
    add        sp, sp, #16
    ret
 
/*
 * -----------------------------------------------------------------------
 * void    display_char_scif(struct sypage_entry *, char)
 *
 * x0: syspage pointer
 * x1: character
 * -----------------------------------------------------------------------
 */
CALLOUT_START(display_char_scif, 0, patch_debug)
    mov        x7, #0xabcd                // UART base address (patched)
    movk    x7, #0xabcd, lsl #16
    movk    x7, #0xabcd, lsl #32
    movk    x7, #0xabcd, lsl #48
 
0:    ldr        w2, [x7, #SCIF_SCFSR_OFF]
    tst        w2, #SCIF_SCSSR_TDFE
    b.eq    0b
 
    and        w0, w1, #0xff
    strb    w0, [x7, #SCIF_SCFTDR_OFF]
 
1:    ldr        w2, [x7, #SCIF_SCFSR_OFF]
    tst        w2, #SCIF_SCSSR_TEND
    b.eq    1b
 
    mov        w2, #0
    strh    w2, [x7, #SCIF_SCFSR_OFF]
 
    ret
CALLOUT_END(display_char_scif)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,774评论 0 27
  • 进程 创建 创建进程用fork()函数。fork()为子进程创建新的地址空间并且拷贝页表。子进程的虚拟地址空间...
    梅花怒阅读 1,875评论 0 7
  • QNX相关历史文章:QNX简介QNX Neutrino微内核QNX IPC机制QNX进程管理器QNX资源管理器QN...
    Loyen阅读 7,921评论 2 5
  • 思维导图https://mubu.com/doc/1y91Dl_sPF 嵌入式系统概述 嵌入式系统(Embedde...
    DecadeHeart阅读 1,368评论 0 6
  • 一次去朋友单位办事,发现一个有趣的事情:朋友单位中层领导多数是80后,唯有他所在的办公室,领导是一个70后。 朋友...
    木南开阅读 555评论 0 0