Archive for the ‘Linux’ category

SSH 使用 — 如何从外网连接内网计算机

June 11th, 2016

SSH是我们经常使用的工具,但是因为现在组网方式非常复杂,导致跨越NAT非常难因应对,同一局域网下的SSH使用,我这里就不再赘述。我们来聊聊如何从外网连接内网的ssh-server。

首先我们可以选择偷懒的方式,设置主机别名,这样就不用每次都输入ssh user@remote -p port 了,我们可以在~/.ssh/config 里面追加以下内容:

Host lab
   HostName xxx.xxx.xxx.xxx
   User user
   Port port

保存之后,即可用 ssh lab 登入,如果还配置了公钥登入,那就连密码都不用输入了。

================

两台主机之间传输数据可以使用scp命令,它与ssh -p port 很类似,稍微的差别在与指定端口时用的是大写的 -P 而不是小写的。有了这些基础知识,下面我们聊聊如何从外网连接内网的ssh-server。为了实现这个功能,我们必须拥有一台公网主机,现在国内的vps非常便宜,阿里云或者腾讯云都可以。

假设现在我有一台处在公网的机器 bridge,这台机器拥有公网IP 。你在实验室也有一台机子lab,这台机子只能在实验室内部访问,但他可以访问公网,你希望能在任何地方都能访问这台机器。那么使用 ssh -R 可以轻松地做到这个事情。

lab$ ssh -R 10022:localhost:22 bridge
bridge$ ssh user@localhost -p 10022
lab$

只要上面这个过程成功了,就说明在你执行 ssh -R 10022:localhost:22 bridge(ssh -R 远端:本地 bridge,如果远端不添加0.0.0.0 就意味着只能bridge自己访问这个lab,如果添加了0.0.0.0就意味着可以其他机器可以通过bridge访问到)之后,你成功地将本地上的 22 端口反向转发到了 bridge 的 10022 端口。只要保持这个 ssh 不断,任何一台机器都可以首先连接到 bridge,然后通过 ssh user@localhost -p 10022 连回到 lab。

不过上面这么做并不稳健,如果因为网络波动导致 ssh -R 那个连接断了,那么从 bridge 就完全失去了到 lab 的控制。万幸的是,有一个叫做 autossh 的软件,可以自动的检测断线,并在断线的时候重连。在 Ubuntu 上你可以使用 sudo apt-get install autossh 来安装。

lab$ autossh -NfR 10022:localhost:22 bridge

上面这句话里面 -N 表示非不执行命令,只做端口转发;-f 表示在后台运行,-R 10022:localhost:22 就是把本地的22端口转发到远程的10022端口。我们也可以设置为开机时运行:在 /etc/rc.local 里面 exit 0 这句话之前加上

su - user -c autossh -NfR 10022:localhost:22 bridge

其中 user 是用户名。需要注意的是,开机时运行 autossh 需要配置公钥登入。

临时创建网站供查看

比如我在本地跑了一个网站,我想临时把我的网站发给朋友看看。在本地运行 python -m SimpleHTTPServer 即可在本地的8000端口启动一个网站,你可以在浏览器中通过 http://localhost:8000/ 看到。下面我们想让远方的朋友看到这个网站。

local$ ssh -NR 0.0.0.0:18000:localhost:8000 bridge

远方的朋友即可通过 http://bridge:18000/ 看到了。注意到这里和上面的命令有一个小小的不同,就是多了 0.0.0.0,这告诉 ssh,要把18000端口绑定在远端的所有IP上。如果像之前那样省略的话,缺省值是只绑定在 localhost,也就是只有在 jumpbox 本机才可以访问,而其他人都不能访问。

临时代理创建

比方说在本地的127.0.0.1:1080运行了HTTP代理服务,现在我想让另一台机子 remote 也能够使用这个HTTP代理。

local$ ssh -NR 11080:localhost:1080 remote
local$ ssh remote
remote$ export http_proxy=http://127.0.0.1:11080/
remote$ export https_proxy=http://127.0.0.1:11080/
remote$ curl http://ifconfig.co

用作 SOCKS5 代理

要是想要在家访问公司内网的一些网站,但是公司又没有提供进入内网的VPN,那怎么办呢?通过 ssh -D 可以在本地建立起一个 SOCKS5 代理:local$ ssh -ND 1080 workplace

 

【1】 ssh manual

如何给 kernel.org 下的项目贡献代码

March 17th, 2016

kernel的开发模式,不用细说了,直接切入正题!

1.  首先你总要把kernel-stable 和 linux-next git仓库克隆下来

2.  修改文件,blabla…. 然后使用 git 生成补丁

git format-patch -1

git 会将最近一次的提交生成补丁文件,可以在当前目录下看到 0001-*.patch 文件

3.  使用邮件发送你的 patch 给 maintainer 与 mailing list

代码库下会有文件描述 maintainer 的信息,这里以 rt-tests 为例,在 MAINTAINERS 文件中有 maintainer 的邮件地址,可将你的patch 发给上述地址。

4.  使用 Mutt 发送 patch
Mutt是一个命令行界面下的邮件客户端,具体配置方式这里就不再赘述,可以参考http://jingyan.baidu.com/article/0aa22375bbc3be88cc0d6425.html 来配置。

===================

如果遇到大patch的话,社区通常会reject,我们需要把patch分成若个小patch,每个patch叙述一部分功能,这样可以方便review,而且使得源码容易维护。那么如何拆分大patch呢?

比如这时在本地我们已经提交了一个大patch,然后可以执行以下操作:

1.  将当前提交撤销,重置到上一次。

$ git reset HEAD^

2.  通过补丁块拣选方式择要提交的修改。 Git 会逐一显示工作区更改,如果确认此处动要 会逐一显示工作区更改,如果确认此处动要提交,输入 “y“。

$ git add -p

以撤销提交的说明为蓝本,撰写新的commit。

$ git commit -e -C HEAD@{1}

3.  如果提交代码过于密集,耦合太强,那么上面这种方式不太适用,那么这时可以 直接编辑文件,删除要剥离出此次提交的修改然后执行:

$ git commit -- amend

然后执行下面的命令,还原有文件修改再提交:

$ git checkout HEAD@{1}-- .
$ git commit

 

参考:

http://jingyan.baidu.com/article/0aa22375bbc3be88cc0d6425.html

手动创建类docker环境

March 10th, 2016

最近docker特别的火,但是细究docker的原理机制。其实就是使用了cgroups+namespace完成资源的限制与隔离。现在我们手动创建一个namespace的容器,做到资源的隔离。

之前我们已经讨论了namespace的组成现在我们通过手动的方式创建每种不同namespace的环境。创建不同的namespace主要使用clone()+特定关键字的方式进行。我们可以把clone返回的pid,所以container也是一种特殊的进程!

Mount namespaces CLONE_NEWNS Linux 2.4.19
UTS namespaces CLONE_NEWUTS Linux 2.6.19
IPC namespaces CLONE_NEWIPC Linux 2.6.19
PID namespaces CLONE_NEWPID Linux 2.6.24
Network namespaces CLONE_NEWNET 始于Linux 2.6.24 完成于 Linux 2.6.29
User namespaces CLONE_NEWUSER 始于 Linux 2.6.23 完成于 Linux 3.8)

 

š 通过这个表格我们看到每个namespace完成时间不同,但是基于目前kernel版本已经为4.0.我们可以理解namespace部分基本完成。首先我们先定义一个模板:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};

int child_main(void* arg)
{
  printf(" - World !\n");
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  printf(" - Hello ?\n");
  int child_pid = clone(child_main, child_stack+STACK_SIZE, SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  return 0;
}

这里我们发现clone()的第二个参数是child_stack+STACK_SIZE,这里要说明下栈是从高地址往低地址走的,所以给出最后一个地址,也就是给出了栈的首地址。

1.UTS namespace[1]

使用这个UTS namespace可以使得容器拥有自己的主机名,程序是要是使用clone+sethostname()配合,这个与fork一个进程特别相似。

// (needs root privileges (or appropriate capabilities))
//[...]
int child_main(void* arg)
{
  printf(" - World !\n");
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  printf(" - Hello ?\n");
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | SIGCHLD, NULL);
  waitpid(child_pid, NULL, 0);
  return 0;
}

与fork()函数类似,fork()函数创建一个新的child_pid时,进程中的内容与父进程完全相同,当后续执行子进程功能时,使用execv()族函数覆盖子进程各个程序段,包括代码段数据段等。这里我们注意到clone()函数中的CLONE_NEWUTS关键字,然后在子函数中调用execv(child_args[0], child_args);

注:child_args省略在运行程序添加参数

运行结果:

lzz@localhost:~/container$ gcc -Wall main.c -o ns && sudo ./ns
 - Hello ?
 - World !
root@In Namespace:~/container$ # inside the container
root@In Namespace:~/container$ exit
lzz@localhost:~/container$ # outside the container

上面的这个namespace只做到主机名的隔离,其他子系统都没有还没有隔离,我们在proc下还是可以看到全局的信息。

2.IPC namespace[2]

这里我们使用pipe进行同步,当创建child_pid时, checkpoint[0]为管道里的读取端,checkpoint[1]则为管道的写入端。当管道没有数据时,read()调用将默认的被阻塞,等待某些数据写入,从而达到同步的目的。

...
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);
  // wait...
  read(checkpoint[0], &c, 1);

  printf(" - World !\n");
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - Hello ?\n");

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);

  // some damn long init job
  sleep(4);
  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

这里在父进程下关闭close(checkpoint[1]);意味着父进程结束,子进程才能继续。

3. PID namespace[3]

PID namespace可以做到容器内部的pid与容器外的隔离,也就是说都可以有pid 1的进程,当然容器内pid 1 的进程映射到容器外,拥有其他的pid 号。

...
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);
  // wait...
  read(checkpoint[0], &c, 1);

  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - [%5d] Hello ?\n", getpid());

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | SIGCHLD, NULL);

  // further init here (nothing yet)

  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

这里我们看到在clone的标志里又加入了CLONE_NEWPID,然后在child_main中加入getpid(),我们可以发现容器的pid号。运行结果:

lzz@localhost:~/container$ gcc -Wall main-3-pid.c -o ns && sudo ./ns
 - [ 7823] Hello ?
 - [    1] World !
root@In Namespace:~/blog# echo "=> My PID: $$"
=> My PID: 1
root@In Namespace:~/blog# exit

这里我们发现在容器中,我们没有挂载proc文件系统,这里有一个问题,如果我们在容器里面挂载一个proc,在容器外使用top、ps -aux会提示出错。需要重新挂载proc在根目录下,因为这里我们并没有隔离文件系统。

4.CLONE_NEWNS[4]

这个clone选项,可以保证在容器内的文件挂载操作,不影响父容器的使用,也就解决了上面proc挂载损坏父容器空间的问题。

...
// sync primitive
int checkpoint[2];
....
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);

  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);

  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);

  // wait...
  read(checkpoint[0], &c, 1);

  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - [%5d] Hello ?\n", getpid());

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);

  // further init here (nothing yet)

  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

这个时候我们运行这个程序:

lzz@localhost:~/container$ gcc -Wall ns.c -o ns && sudo ./ns
 - [14472] Hello ?
 - [    1] World !
root@In Namespace:~/blog# mount -t proc proc /proc
root@In Namespace:~/blog# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  1.0  0.0  23620  4680 pts/4    S    00:07   0:00 /bin/bash
root        79  0.0  0.0  18492  1328 pts/4    R+   00:07   0:00 ps aux
root@In Namespace:~/blog# exit

可以发现容器内的ps读取的是容器内部的proc文件系统!

这里我们就要考虑docker中,比如我们从docker hub下pull下一个镜像,在这个镜像中必然存在一个文件系统rootfs,包括各种配置信息,基本的链接库。各种特殊的文件系统:/dev,/sysfs等。这个就需要我们进行裁剪!
这里主要存在的目录有:

bin etc lib lib64 mnt proc run sbin sys tmp usr/bin 

我们需要根据自己的architecture配置这些目录。最后使用mount,将容器中的文件系统,都挂载到这些目录下。挂载完毕后使用chroot与chdir隔离目录:

    if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
        perror("chdir/chroot");
    }

5.User namespace[5]

这个namespace主要是管理用户的UID,GID,主要原理通过读写/proc//uid_map 和 /proc//gid_map 这两个文件。这两个文件的格式为:ID-inside-ns ID-outside-ns length 。
核心函数:

void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}

void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

6.Network namespace[6]

这个namespace主要完成的是将一块物理网卡虚拟出多快虚拟网卡,主要命令:

# Create a "demo" namespace
ip netns add demo

# create a "veth" pair
ip link add veth0 type veth peer name veth1

# and move one to the namespace
ip link set veth1 netns demo

# configure the interfaces (up + IP)
ip netns exec demo ip link set lo up
ip netns exec demo ip link set veth1 up
ip netns exec demo ip addr add xxx.xxx.xxx.xxx/30 dev veth1
ip link set veth0 up
ip addr add xxx.xxx.xxx.xxx/30 dev veth0

运用在代码中就是:

...
int child_main(void* arg)
{
  char c;

  // init sync primitive
  close(checkpoint[1]);

  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);

  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);

  // wait for network setup in parent
  read(checkpoint[0], &c, 1);

  // setup network
  system("ip link set lo up");
  system("ip link set veth1 up");
  system("ip addr add 169.254.1.2/30 dev veth1");

  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}

int main()
{
  // init sync primitive
  pipe(checkpoint);

  printf(" - [%5d] Hello ?\n", getpid());

  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, NULL);

  // further init: create a veth pair
  char* cmd;
  asprintf(&cmd, "ip link set veth1 netns %d", child_pid);
  system("ip link add veth0 type veth peer name veth1");
  system(cmd);
  system("ip link set veth0 up");
  system("ip addr add 169.254.1.1/30 dev veth0");
  free(cmd);

  // signal "done"
  close(checkpoint[1]);

  waitpid(child_pid, NULL, 0);
  return 0;
}

对于网络这一块,要想使得容器内的进程与容器外的进程通信,需要架设网桥,具体可以查看相关文档。

Final

我们这里看一下父子容器的namespace:
父:

lzz@localhost:~$ sudo ls -l /proc/4599/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026531838]

子:

lzz@localhost:~$ sudo ls -l /proc/4600/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026532520]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026532522]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026532521]

我们可以发现,其中的ipc,net,user是相同的ID,而mnt,pid,uts都是不同的。如果两个进程指向的namespace编号相同,就说明他们在同一个namespace下,否则则在不同namespace里面。

 

参考:

[1] http://lwn.net/Articles/531245/

[2] http://coolshell.cn/articles/17010.html

[3] http://lwn.net/Articles/532741/

[4] http://coolshell.cn/articles/17029.html

[5] http://lwn.net/Articles/539941/

[6] http://lwn.net/Articles/580893/

[7]

https://blog.jtlebi.fr/2014/01/12/introduction-to-linux-namespaces-part-4-ns-fs/
http://www.cnblogs.com/nufangrensheng/p/3579378.html

setjmp和longjmp的另类使用

February 3rd, 2016

C语言的运行控制模型,是一个基于栈结构的指令执行序列,表现出来就是call/return: call调用一个函数,然后return从 一个函数返回。在这种运行控制模型中,每个函数调用都会对应着一个栈帧,其中保存了这个函数的参数、返回值地址、局部变量以及控制信息等内容。当调用一个 函数时,系统会创建一个对应的栈帧压入栈中,而从一个函数返回时,则系统会将该函数对应的栈帧从栈顶退出。正常的函数跳转就是这样从栈顶一个一个栈帧逐级地返回。

setjmp的返回值:直接调用该函数,则返回0;若由longjmp的调用,导致setjmp被调用,则返回val(longjmp的第二个参数)。

之前看APEU的相关章节,setjmp和longjmp只是一个跨函数跳转的库函数调用,可以作为后悔药使用,但是今天我发现这个库函数可以作为协程使用。协程我之前一直不理解,认为有了进程线程就可以了,没有必要存在协程,但是发现在不支持这些多线程多进程的操作系统平台上协程意义重大。

这个时候协程就可以派上用场了,我们可以依赖协程模拟多进程这种需求,我们需要写一个thread库供协程调用,具体的thread工作步骤就是:

  1. 存储当前线程所在上下文,设置一个存储队列专门存储thread context
  2. 为每个线程分配一个stack空间
  3. 将esp指向栈顶,eip指向要执行代码的entry,当然包括参数arg,arg具体调用方式就是(current->entry(current->arg)),这一个非常相似于c++中的委托
  4. 当需要调度线程时,将当前执行代码设置setjmp,保存线程结构体中的thread context到具体全局的数组
  5. 如果需要调度另外一个线程,使用longjmp跳入到线程结构thread context

当然了在linux下有glibc提供相关库函数实现跳转,咱们不必再次造轮子,但是在裸机上,或者一种新的体系结构中,我们必须自行实现setjmp和longjmp,这其中不可避免的会使用到asm。比如setjmp,首先要将返回地址和frame pointer压入栈,考虑到栈自高地址向低地址方向生长,故esp-8,然后再压入其他通用寄存器。而longjmp恢复某个线程·的上下文环境,必须指定存储context位置 ,然后将返回地址复制给eax,然后执行跳转。

struct jmp_buf
{
       unsigned j_sp;  // 堆栈指针寄存器
       unsigned j_ss;  // 堆栈段
       unsigned j_flag;  // 标志寄存器
       unsigned j_cs;  // 代码段
       unsigned eip;  // 指令指针寄存器
       unsigned ebp; // 基址指针
       unsigned edi;  // 目的指针
       unsigned j_es; // 附加段
       unsigned j_si;  // 源变址
       unsigned j_ds; // 数据段
};

具体线程切换伪代码:

void wthread_yield()
{
   ...
   if(current){
        if(setjmp(current->...)!=0)
             return;
        push(...)
   }
   current = next;
   longjmp(current->...)
}

考虑到执行setjmp和longjmp必须是一个控制main线程,必须由控制线程控制调用线程切换,其他线程可以主动让出时间片。这时我们必须定义一个全局变量保存线程上下文,然后维护这个数组,至于具体的逻辑形式可以是队列可以是环形队列队列等。编写thread库务必保证线程安全,不能破坏线程返回地址,否则容易core dump。

另外在linux下,可以使用这两个系统调用实现C下的异常处理try/catch,至于在setjmp和longjmp之前存在的变量务必使用volatile声明。

 

 参考:

http://stackoverflow.com/questions/2560792/multitasking-using-setjmp-longjmp#comment33335405_2560792
http://www.cnblogs.com/lq0729/archive/2011/10/23/2222117.html
http://www.cnblogs.com/lienhua34/archive/2012/04/22/2464859.html

用 qemu 来调试 Kernel

January 13th, 2016

引言

  • kgdb 方式

kgdb 的方式需要两台电脑,一台是宿主机,另一台是开发机,在开发机上编译打好补丁的内核代码,然后拷贝到宿主机上运行。注:目前 kgdb 支持的版本比较低了,好像在 2.6.19 左右,如果需要调试高版本的内核比较麻烦,而且需要通过串口方式调试,必须需要两台电脑,安装配置也比较麻烦,不过该方式调试比较准确,不会因为优化问题而无法查看变量。

  • uml 方式

uml ( user mode linux kernel ),是一种在用户态调试内核的方式,该调试方式在 2.6 就进入主线了。在源码包中,进入 arch/um 文件夹,就能看到该方式。该方式存在问题是无法调试硬件相关,如果你只需要调试调度、调试文件系统等,那么你可以使用,该方式比较简单,可自行百度。

  • printk 方式

这个方式也就是说在想调试的地方打印调试信息,需要反复的编译,反复增减调试信息是比较繁琐的一个地方。

qemu 调试内核

建议不要从源中拉版本安装,因为可能源中的版本太低,这个问题困扰了我很久,如果版本太低的话,导致文件系统加载的时候会出现故障,会出现以下提示: cannot load filesystem…
首先从 http://wiki.qemu.org/Download 下载最新版的 qemu 源码,我下的是: qemu-2.4.0.tar.bz2 版本,按照下列方式安装就可以了。

$tar -xvf qemu-2.4.0.tar.bz2
$cd qemu-2.4.0
$./configuration
$make install

我就默认安装的,并没有修改安装地址,如果有需要的话请自行定制。

源码配置

$tar -xvf linux-source**
$cd linux-source**
$vim Makefile

编辑 Makefile 文件,将所有 -O2 优化方式修改为 -O0 ,这样可以部分减少查看变量时的 optimized 提示(也就是说变量被编译器优化了,放到寄存器了,无法打印)。

$make menuconfig

接下来修改内核调试选项

kernel将上述选项都选中。 然后就是 make bzImage。这样就会在 arch/i386/boot/ 下生成 bzImage 文件,内核部分就结束了。

文件系统制作

这块我主要是借鉴了网上的一个帖子: http://blog.csdn.net/wesleyluo/article/details/7943087 该帖子详细讲解了如何制作根文件系统,如果你遇到跟我一样的问题,就是制作的根文件系统无法使用的话,也就是提示找不到 filesystem ,那么请你转到 buildroot 工具,制作根文件系统。

gdb 调试

接下来就是调试你的内核啦,不要太激动啊,因为你还是有可能遇到文件系统无法加载啊, qemu 调试报错啊等等问题。
制作一个脚本来快速启动 qemu 调试

#!/bin/bash
qemu-system-i386 -kernel linux_path/arch/i386/bzImage -hda rootfs.ext2 -append "root=/dev/sda rw" -s -S

关于这个 shell 可能有些疑惑, -kernel 就是使用后边的 bzImage 作为内核镜像。 -hda 我的理解就是作为硬盘引导项, -s 是 gdb 调试的快捷方式相当于 -gdb tcp::1234, 打开一个 gdbserver 在 TCP 端口 1234.-S 选项是启动之后就暂停,等待用户命令。

然后新打开一个终端,输入

$gdb linux_path/vmlinux

等待 gdb 把符号加载完成,加载提示:

$Reading symbols from ..../vmlinux ...done

这样就加载完成了,接下来输入:

$target remote localhost:1234
$b start_kernel
$c

就开始运行然后停在了 start_kernel , OK 大功告成了。

参考:

http://blog.csdn.net/wesleyluo/article/details/7943087