Authuir Notepad

Saving Memory

RTTI、反射

前言

  说实话,弄清楚这几个概念花了我很长的时间。但是弄清楚这几个概念是非常有必要的,这篇文章是我对这几个概念的总结。

RTTI

  RTTI的英文全称是"Runtime Type Identification",它指的是程序在运行的时候才确定需要用到的对象是什么类型的。直观一点来看,举个例子:目前有一个方法可能返回不同类的实例,如何确定返回的到底是什么类呢?这就是所谓的运行的时候才确定类型,也就是所谓的动态多态。在 C++ 中通过 typeid 和 dynamic_cast 实现了 RTTI。

  与动态多态相对应的就是静态多态。在 C++ 里静态多态一般指的是用模版和重载,在编译时就能确定对象类型的多态方法。

阅读

逐帧分析系统调用

系统调用的基本概念

  Linux内核提供了一组用来实现各种系统功能的API函数,这就称为系统调用。系统调用除了将底层接口(例如驱动)的实现包装了起来,还能做到权限控制以及保护。试想一下,假设现在任意程序都能绕开系统调用直接操纵底层接口,例如能绕开页表向任意的物理内存地址写入或者读取数据,那么进程之间的数据隔离也就无法做到了,严重一点地说整个系统内核都能被任意进程破坏,也就没有秩序可言。

  为了解决这个问题,现代操作系统一般都在硬件级(也就是CPU)支持指令集权限控制,例如Intel的x86指令集处理器就提供了4个等级的状态:ring0-ring3,CPU在某一时刻可能处于这4个状态的其中一个,在ring0状态下,CPU能够调用所有指令,而在ring3状态下,CPU仅能调用基本的运算指令。

  而为什么操作系统能占据Ring0状态,而进程无法进入Ring0状态呢,这就要从系统启动过程说起了。

系统启动过程分析

  计算机从上电开始是如何一步步从硬盘加载操作系统的,这篇文章讲得已经很清楚了。在Bootloader执行之后,位于/boot目录下的Linux内核会被加载到内存并执行,这个过程又分为两步:第一步是在实模式下执行boot/setup.bin,这一步主要也是执行一些初始化的操作和硬件的检查工作;第二步是在保护模式下执行boot/vmlinux.bin,在这个过程中会初始化页表、开启分页机制,最终会跳转到start_kernel()函数,到此为止内核便加载成功了。之后会执行/sbin/init,这便是Linux系统中的第一个进程。

  上面提到了实模式保护模式,下面就来解释一下。实模式指的就是实地址模式,顾名思义,实模式是将整个物理内存看成分段的区域,程序代码和数据位于不同区域,系统程序和用户程序没有区别对待,而且每一个指针都是指向"实在"的物理地址。而保护模式保护的就是物理内存的访问,通过段式内存管理或者页式内存管理将请求的内存地址映射到实际的物理地址,而段表或页表只有系统内核才有权限修改,从而通过这种方式实现了内存访问的控制。

  计算机的外设都会通过各种方式挂载在系统总线上的,具体来说,挂载的设备在CPU上能对应上一些物理内存地址,而通过读写这些内存地址就能达到控制外设(网卡、声卡)或者读写外设(磁盘)的目的。在实现了对物理内存访问控制之后,便能够限制普通进程操纵这些设备。

  在进入保护模式之后,进程所能够寻址的地址就全都由内核通过页表来控制了。

进程启动过程分析

  上面一节解释了为什么用户的进程无法访问实际的物理地址,接下来就要介绍一下为什么进程无法运行ring0权限下的指令。

  在Linux中能通过fork()或者exec()函数创建一个进程。以exec函数族为例,其中一个声明是这样的:int execv(const char *path, char *const argv[]);,它会把第一个参数路径对应的可执行文件(例如ELF)加载到内存,紧接着执行分配资源(例如页表)、拷贝环境变量和命令行参数、将此进程加入进程调度队列开始调度和执行,至此大致的进程启动过程便结束了。

  那么有一个问题,Linux内核将时间片分配给这个进程并将CPU交给此进程来使用,那么在此进程的时间片耗尽或者需要重新调度的时候,Linux内核是如何重新获得CPU的使用权的呢。

  在内核加载的过程中,所有的中断向量表对应的地址都会被写入Linux内核的相应的处理函数的基址。简单一点来说,只需要定义一个硬件定时器(这个由CPU的硬件实现),并将这个定时器的中断向量注册为Linux的相应处理函数,那么时间一到,CPU会自动打断当前正在运行的代码,并将PC指向定时器的中断向量,此时Linux内核便重新获取到了CPU的使用权。

  到了这一步就能解释为什么操作系统能占据ring0状态,而普通进程无法进入ring0状态了。原因很简单,Linux在将CPU使用权交给进程之前,主动从ring0退到ring3,这个过程是不可逆的,因而普通进程执行的全过程都是在ring3下的。而在定时器中断或者其他硬件中断被捕获到的时候,CPU会恢复到ring0,而此时CPU的控制权又已经在Linux内核手中了,所以在一般情况下普通进程是无法得到ring0权限的。

系统调用过程

  虽然普通进程无法在ring0权限下操纵底层设备或者执行命令,但是很多情况下往往也是需要和设备进行交互的(例如读写磁盘),因而系统调用应运而生。

  下面来分析一个简单的系统调用过程,考虑一段最简单的系统调用代码:

#include <unistd.h>

int main() 
{
    char* message = "Hello, World!";
    write(1, message, 13);
    return 0;
}

  write的第一个参数代表stdout,这个程序会在屏幕上打印出字符串message。如前面所说,普通进程是无法操纵底层设备的,那么毫无疑问,write中执行了系统调用,Linux内核帮我们完成了将字符串输出到屏幕的这个过程。来看一下write函数在32bit下的汇编实现:

#include <sys/linux-syscalls.h>

    .text
    .type write, @function
    .globl write
    .align 4

write:
    pushl   %ebx
    pushl   %ecx
    pushl   %edx
    mov     16(%esp), %ebx
    mov     20(%esp), %ecx
    mov     24(%esp), %edx
    movl    $__NR_write, %eax
    int     $0x80
    cmpl    $-129, %eax
    jb      1f
    negl    %eax
    pushl   %eax
    call    __set_errno
    addl    $4, %esp
    orl     $-1, %eax
1:
    popl    %edx
    popl    %ecx
    popl    %ebx
    ret

  可以很清楚的看到,在将参数保存到寄存器后,write函数调用了int指令产生了一个软件触发的硬件中断。此时如上面所说,CPU的控制权将在此交接给Linux内核,并执行Linux相应的中断处理函数。中断处理函数只需读取eax寄存器,就能够知道是要执行哪个系统调用(此处为__NR_write),内核根据这个交给不同的系统调用去处理。处理完毕后内核将执行调度,并降低运行权限,再将CPU运行权交给下一个就绪的进程。

  (注:在64bit模式下,系统调用不再使用int指令,转而使用syscall指令)

使用机器学习构建自动问答系统

起因

  其实这个项目是我今年上半年的时候参加微软编程之美2017的时候的参赛作品,也是我的第一个比较完整的 NLP 的应用,在Task1 里的 MRR 分数应该是是排名前几的,所以来介(chui)绍(yi)一(bo)下。

  这个比赛会给你一堆问题和有关这个问题的正确答案和错误答案,然后在测试集中将给出另外一些问题以及和这个问题相关的一些答案,要求你将这些答案排序,正确答案越靠前则分数越高。下面是我的模型的部分测试结果:

Demo

阅读

如何正确使用卷积神经网络给学校教务系统添堵

前言

  在我本科阶段的学习中,一大乐趣就是和学校的网络管理员谭X斗智斗勇。我之前所在的工作室能用各种官方的、非官方的手段拿到学校的一些数据,然后我们工作室的一大目标就是利用这些数据来让各位同学更加方便地使用学校的各项功能(例如选课、成绩查询之类的功能)。

  上面说的官方的办法就是通过指导老师出面,要到一些关键数据库的帐号密码,而非官方的办法也很粗暴,就是利用用户托管在我们平台的帐号去爬取数据。所以网络中心的谭x老师对我们还是非常恼火的,时不时就会上门找茬,例如这次,他就在我们爬取数据的登录接口加了一个验证码。

阅读

使用Visual Studio远程编译Nginx on Linux并远程Debug

为什么要使用VS开发

  和普通科班出身的人不同,我的 C++ 开发入门时候用的 IDE 应该就是Visual Studio 2012,而且自己写的一些小软件也基本上是在 windows 上跑跑而已,所以基本没有怎么用过 gdb 。等到了我接到 linux 开发需求的任务时已经为时已晚。虽然说此类 debugger 无非就是干三件事情:代码断点、变量监视、查看堆栈,但是用惯了VS系列 IDE 的我发现我已经不能接受gdb这种命令行下的 debugger 软件了。

  万幸的是,在今年的 Microsoft Build 2017 Developer Conference 上,VS 开始原生支持远程编译、远程 debug ,这意味着我可以用 Linux 里的 gcc/g++ 去编译、用 gdb 去调试,再在最外面用 VS 这个“壳子”来让这个过程图形化,可以说是非常酷炫了。

这次的挑战

  这次我遇到的一个任务是给某公司的 Load Balance 服务器上的 nginx 开发一个定制插件。这个插件包括了以下功能:

  • 能通过 HTTP 请求的 Header 中的 UID 信息来区分不同的用户
  • 动态查询 UID 对应的不同用户的限速值并给予限速(上行、下行)
  • 实现一定的负载均衡功能

  仅仅就限速这个功能来说,还是非常好实现的,现成的插件也是一大堆,再不济用 openresty 也是没问题的,但是动态限速和 UID 获取就得再对现成的限速代码做一定修改了。

  由于 nginx 的插件需要连同 nginx 一块编译,所以在 linux 平台上调试起来对于我这个 gdb 小苦手也是非常难受的,所以这一次我决定用一下 VS2017 提供的远程 Debug 新功能。

相关介绍

  对于这个新功能的详细介绍请参看这里,讲解得非常清晰,我就说说自己的一些理解吧。

  这个 Remote Debugger 的底层实质还是 gdb-server ,所以它的写代码到编译到调试的过程实质上是这样的:

  • 首先在 VS 的 IDE 中修改了代码之后,代码会保存到硬盘,在按下 Remote Debugger 按钮之后,VS 会通过你预先输入的 SSH 帐号密码使用 SCP 传输到远程 Linux 机器上。
  • 然后 VS 将通过 SSH 远程使用 GCC 编译源代码,若出现错误,则将错误传输到 Windows 的 VS 界面上,如果没有错误,将生成二进制可执行文件。
  • VS 再通过 gdb-server 打开一个端口供远程主机访问,然后 VS 接入端口,即可使用 gdb 对远程机器上跑的程序进行调试,VS 再把这个过程在界面中可视化,这样就做到了在 VS 中写、调试跑在 Linux 上的代码。

阅读