Ubuntu 8.04 长期支持桌面版本 Linux 发布

[本博讯,北京时间2008年4月24日19点] Canonical 公司发布了最新的开源 Linux 操作系统发行版:Ubuntu8.04 LTS(长期支持)桌面版,Canonical 也同时发布了 Ubuntu 8.04 长期支持服务器版。

Ubuntu(音:oo-BOON-too[u:'bu:ntu:]) 8.04 LTS(Hardy Heron) 是 Canonical 公司自 Ubuntu 6.06 LTS 以来发行的第二个长期支持版本,它将提供对桌面版 3 年,服务器版 5 年的长期升级和服务支持。Linux Foundation 2007 年的调查结果显示,Ubuntu Linux 已经成长为桌面市场的后起之秀,占据了 Linux 桌面版本的头把交椅。同样,这次 Ubuntu 8.04 将以更完善的硬件支持,更漂亮的用户界面,更新更多的软件包选择,更方便的用户交互掀起一场 Linux 桌面系统的大革命。

Ubuntu 8.04 的完整下载地址列表:http://www.ubuntu.com/getubuntu/downloadmirrors

Ubuntu 8.04 的系统安装指导,常用程序安装,桌面效果截图,可以在这里找到。

如果你是新手也没有关系,一个小软件 Wubi 允许你在 Windows 下像安装应用程序一样轻松安装 Ubuntu Linux

另外,中国北京的清北DIY俱乐部目前宣布向全国提供免费 Ubuntu 光盘派送,每人可免费申请一套Ubuntu 8.04系统光盘(1DVD+1CD),详情见http://quickbest.com.cn/discuz/thread-26153-1-1.html

PS: 秀一个别人做的 Ubuntu Sticker:

Image

在 Dell Latitude D630 上硬盘安装 Ubuntu Linux 8.04

最后更新时间:2008年4月18日

摘要

这份文档主要描述了我在自己的 Dell Latitude D630 上安装 Ubuntu Linux 8.04 的过程。

目录

1. 介绍
2. 安装
3. 效果截图
4. 总结

1. 介绍

虽然一直想忍着等 Ubuntu Linux 8.04(Hardy Heron) 正式发布了再安装它,但最后还是受不了诱惑,在还有 6 天就发布时候当了一次小白。这次 Ubuntu 真的是让我相当相当满意,安装过程什么问题都没有出,所有硬件均一步识别,太爽了!这次写的安装介绍,要比上次那篇简单多了,我真心希望以后的 Ubuntu 版本安装的时候再也不用看别人的安装介绍了。

而且这次 Ubuntu Studio 的 theme 也比 7.10 漂亮许多,Compiz-fusion 的 3D 桌面特效也更好了,真酷!兄弟姐妹们,来玩 Ubuntu 吧!

2. 安装

[安装基本操作系统]
由于我电脑上本来就有 Ubuntu 7.10 和 Cent OS 5.1,所以我的 Ubuntu 8.04 是通过修改 grub 的 menu.lst 来进行硬盘安装的。

首先,下载安装光盘镜像文件:
$ wget -c http://cdimage.ubuntu.com/daily/current/hardy-alternate-i386.iso
由于现在 Ubuntu 8.04 还没有正式发布,所以我是从 daily build 的地址下的,当看到这篇文章时可能它已经发布了,那就请到官方公布的地址下吧。

其次,下载硬盘安装文件:
$ wget http://archive.ubuntu.com/ubuntu/dists/hardy/main/installer-i386/current/images/hd-media/vmlinuz
$ wget http://archive.ubuntu.com/ubuntu/dists/hardy/main/installer-i386/current/images/hd-media/initrd.gz

将这三个文件放在某个分区根目录下,再修改 /boot/grub/menu.lst,在列表最后添加上以下几行:
title Ubuntu 8.04 Install Entry
root (hd0,3)
kernel /vmlinuz root=/dev/ram ramdisk_size=100000 devfs=mount,dall
initrd /initrd.gz
请注意第二行的(hd0,3)是指放镜像文件那个分区编号,请根据自己情况自行修改。一般情况下如果放到了 /dev/sdan 下的话,这个就应该是 (hd0,n-1)。

然后重启电脑,在 grub 中选择引导项 Ubuntu 8.04 Install Entry 进行安装,安装过程不赘述。我一般选择系统语言为英语,地域为中国。

[修改更新源列表]
选择速度最快的源,将 /etc/apt/sources.list 中默认的官方源替换掉,比如我的 sources.list 文件内容就是
deb http://debian.ustc.edu.cn/ubuntu/ hardy main restricted universe multiverse
deb http://debian.ustc.edu.cn/ubuntu/ hardy-updates main restricted universe multiverse
deb http://debian.ustc.edu.cn/ubuntu/ hardy-security main restricted multiverse universe
deb http://debian.ustc.edu.cn/ubuntu/ hardy-backports main restricted universe multiverse

[更新操作系统]
$ sudo apt-get update
$ sudo apt-get upgrade

[安装受限驱动]
在更新完操作系统后,System Notifacation Area(桌面右上角) 中会出现提示安装受限驱动的气泡,点击安装即可。

[安装中文输入法]
在 System->Administration->Language Support 中选择安装中文支持,并在下方勾选上 Enable support to enter complex characters. 系统会自动下载并安装中文支持和中文输入法,安装完后系统会要求重启,重启以后就可以在软件中直接用 Ctrl+Space 调出输入法了。^_^ 比以前的版本配置中文输入法简单太多了!!!

[安装 Windows XP 中文字体]
这是我写的一个小脚本,可以将双系统的 XP 字体拷贝到 Linux 下并使用它们。注意,这将涉及到版权问题,如果不确信的话,请跳过这一步。
$ wget http://share.solrex.org/scripts/install_win_CN_fonts_to_linux.sh
$ sudo sh install_win_CN_fonts_to_linux.sh
然后重新登录 X window

[安装 Ubuntu Studio 主题]
Ubuntu Studio 的黑色桌面主题是我非常喜欢的桌面主题,再配上诺贝尔和平奖得主戈尔的获奥斯卡最佳纪录片奖的电影 An Inconvenient Truth 的灰色背景海报,简直太配了。如果不喜欢,请跳过这一步。
$ sudo apt-get install ubuntustudio-theme
然后在 System->Preference->Appearance 中选择使用 Ubuntu Studio Theme。

[安装 Compiz-Fusion 3D 桌面特效引擎]
$ sudo apt-get install compiz-fusion-plugins-* compizconfig-settings-manager emerald
然后在 System->Preference->Advanced Desktop Effect Settings 中配置喜欢的桌面特效。

[安装 Mplayer/SMplayer 媒体播放器以及解码器]
$ sudo apt-get install mplayer smplayer
下载最新解码器:
$ wget -c http://www.mplayerhq.hu/MPlayer/releases/codecs/all-20071007.tar.bz2
将最新解码器库扔到 /usr/lib/win32 目录下:
$ tar -xjvf all-20071007.tar.bz2
$ sudo mkdir /usr/lib/win32
$ sudo mv all-20071007/* /usr/lib/win32
这样就可以使用 Mplayer/SMplayer 播放几乎任何格式的影片了。

[安装 星际译王(StarDict) 和辞典包]
$ sudo apt-get install stardict
然后到星际译王官方网站下载安装需要的辞典包

[安装 MP3 播放器 Amarok/xmms]
$ sudo apt-get install amarok

[安装 BBS 登录软件 Qterm]
$ sudo apt-get install qterm

[安装聊天工具 Pidgin, Eva]
$ sudo apt-get install pidgin eva

[安装邮件客户端 ThunderMail]
$ sudo apt-get install mozilla-thunderbird

[安装系统启动项管理软件 sysv-rc-conf]
$ sudo apt-get install sysv-rc-conf

[安装 vim 完全版(gvim)]
$ sudo apt-get install vim-full

[安装编译工具库, g++ 和 javac]
$ sudo apt-get install libc6-dev g++ g++-4.2 sun-java6-jdk

[安装压缩解压缩工具 unzip, unrar, 7zip]
$ sudo apt-get install unzip unrar p7zip-full

[安装 Internet 时间同步工具 ntp]
$ sudo apt-get install ntp

[安装程序开发工具 kscope, eclipse, SVN tool, 十六进制文本编辑器, 网络文件系统]
$ sudo apt-get install kscope eclipsesubversion hexedit nfs-client

[安装工程图画图工具]
$ sudo apt-get install dia kivio

[安装摄像头查看工具]
$ sudo apt-get install cheese

[安装 CHM 文件查看工具]
$ sudo apt-get install chmsee kchmviewer

[安装 tex 编辑编译工具]
$ sudo apt-get install texlive cjk-latex kile

[安装 dos unix 文件转换工具]
$ sudo apt-get install tofrodos

[安装 Adobe Reader]
$ sudo apt-get install acroread
如果上面命令不管用,用下面方法下载安装:
$ wget -c http://debian.cn99.com/ubuntu-cn/dists/gutsy/main/binary-i386/adobe/AdobeReader_chs-8.1.1-1.i386.deb
$ sudo dpkg -i AdobeReader_chs-8.1.1-1.i386.deb

3. 效果截图

Amarok 播放器

4. 总结

Ubuntu 8.04 对电脑硬件的支持又进了一步,至少在我以前曾经有问题的显卡和声卡上都没有出现问题,让人很开心。而更好看的桌面主题,更酷的 3D 桌面效果,更简化的安装过程,更完善的多语言支持,真的很赞,我相信 Ubuntu 将借此进一步巩固和扩大它在 Linux 桌面市场占有率。大家都来用 Ubuntu 吧,它可 Windows 酷多了!

中科院 IP 网关 Linux 登录客户端版本 1.0 发布

愚人节和大家开了个 小玩笑,不过这次可不是玩笑了。CAS NET 正式发布版本 1.0,官方主页:http://share.solrex.org/casnet/

CAS Net 是中科院内部 IP 控制网关的 Linux 登录客户端,此软件完全使用 Python 语言写成,同时支持命令行和图形界面,使用简单,安装方便,实乃中国科学院 Linux 使用者居家旅行必备之良品 :)。

最新版本(1.0)特性:

1. 客户端同时具有命令行和图形界面,满足不同用户需要。
2. 可设置选项多,拥有较高扩展性。
3. 可保存用户设置,登录简单快捷。
4. 纯 Python 编程,修改简单,扩展性强,可移植到不同操作系统平台。
5. 开放源代码,确保程序无后门。

软件效果截图:

Ubuntu 7.10 系统下截图
Ubuntu 7.10 系统下截图

中科院 IP 网关 Linux 登录客户端版本 1pre 发布-注意日期

更多请访问官方主页:http://share.solrex.org/casnet/

CAS Net 是中科院内部 IP 控制网关的 Linux 登录客户端,此软件完全使用 Python 语言写成,同时支持命令行和图形界面,使用简单,安装方便,实乃中国科学院 Linux 使用者居家旅行必备之良品 :)。

最新版本(1 pre)特性:

  1. 客户端同时具有命令行和图形界面,满足不同用户需要。
  2. 可设置选项多,拥有较高扩展性。
  3. 可保存用户设置,登录简单快捷。
  4. 纯 Python 编程,修改简单,可移植到不同 Linux 平台。

感谢列表:

  • giv<goldolphin[at]163.com>: 命令行客户端脚本的原型作者

版权声明:

Copyright (C) 2008 Wenbo Yang<http://solrex.org> 祝大家节日快乐!

本软件遵从 GPL 协议<http://www.gnu.org/licenses/gpl.txt>,在此协议保护之下,您可以自由地使用、修改或分发本软件。

Ubuntu 7.10 系统下截图CentOS 5.1 系统下截图

POSIX 线程取消点的 Linux 实现

摘要:

这篇文章主要从一个 Linux 下一个 pthread_cancel 函数引起的多线程死锁小例子出发来说明 Linux 系统对 POSIX 线程取消点的实现方式,以及如何避免因此产生的线程死锁。

目录:

1. 一个 pthread_cancel 引起的线程死锁小例子
2. 取消点(Cancellation Point)
3. 取消类型(Cancellation Type)
4. Linux 的取消点实现
5. 对示例函数进入死锁的解释
6. 如何避免因此产生的死锁
7. 结论
8. 参考文献

1. 一个 pthread_cancel 引起的线程死锁小例子

下面是一段在 Linux 平台下能引起线程死锁的小例子。这个实例程序仅仅是使用了条件变量和互斥量进行一个简单的线程同步,thread0 首先启动,锁住互斥量 mutex,然后调用 pthread_cond_wait,它将线程 tid[0] 放在等待条件的线程列表上后,对 mutex 解锁。thread1 启动后等待 10 秒钟,此时 pthread_cond_wait 应该已经将 mutex 解锁,这时 tid[1] 线程锁住 mutex,然后广播信号唤醒 cond 等待条件的所有等待线程,之后解锁 mutex。当 mutex 解锁后,tid[0] 线程的 pthread_cond_wait 函数重新锁住 mutex 并返回,最后 tid[0] 再对 mutex 进行解锁。

1  #include <pthread.h>
2
3  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
4  pthread_cond_t   cond = PTHREAD_COND_INITIALIZER;
5
6  void* thread0(void* arg)
7  {
8    pthread_mutex_lock(&amp;mutex);
9    pthread_cond_wait(&amp;cond, &amp;mutex);
10   pthread_mutex_unlock(&amp;mutex);
11   pthread_exit(NULL);
12 }
13
14 void* thread1(void* arg)
15 {
16   sleep(10);
17   pthread_mutex_lock(&amp;mutex);
18   pthread_cond_broadcast(&amp;cond);
19   pthread_mutex_unlock(&amp;mutex);
20   pthread_exit(NULL);
21 }

22 int main()
23 {
24   pthread_t tid[2];
25   if (pthread_create(&amp;tid[0], NULL, &amp;thread0, NULL) != 0) {
26     exit(1);
27   }
28   if (pthread_create(&amp;tid[1], NULL, &amp;thread1, NULL) != 0) {
29     exit(1);
30   }
31   sleep(5);
32   pthread_cancel(tid[0]);
33
34   pthread_join(tid[0], NULL);
35   pthread_join(tid[1], NULL);
36
37   pthread_mutex_destroy(&amp;mutex);
38   pthread_cond_destroy(&amp;cond);
39   return 0;
40 }

看起来似乎没有什么问题,但是 main 函数调用了一个 pthread_cancel 来取消 tid[0] 线程。上面程序编译后运行时会发生无法终止情况,看起来像是 pthread_cancel 将 tid[0] 取消时没有执行 pthread_mutex_unlock 函数,这样 mutex 就被永远锁住,线程 tid[1] 也陷入无休止的等待中。事实是这样吗?

2. 取消点(Cancellation Point)

要注意的是 pthread_cancel 调用并不等待线程终止,它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达 某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。pthread_cancel manual 说以下几个 POSIX 线程函数是取消点:

pthread_join(3)
pthread_cond_wait(3)
pthread_cond_timedwait(3)
pthread_testcancel(3)
sem_wait(3)
sigwait(3)

在中间我们可以找到 pthread_cond_wait 就是取消点之一。

但是,令人迷惑不解的是,所有介绍 Cancellation Points 的文章都仅仅说,当线程被取消后,将继续运行到取消点并发生取消动作。但我们注意到上面例子中 pthread_cancel 前面 main 函数已经 sleep 了 5 秒,那么在 pthread_cancel 被调用时,thread0 到底运行到 pthread_cond_wait 没有?

如果 thread0 运行到了 pthread_cond_wait,那么照上面的说法,它应该继续运行到下一个取消点并发生取消动作,而后面并没有取消点,所以 thread0 应该运行到 pthread_exit 并结束,这时 mutex 就会被解锁,这样就不应该发生死锁啊。

3. 取消类型(Cancellation Type)

我们会发现,通常的说法:某某函数是 Cancellation Points,这种方法是容易令人混淆的。因为函数的执行是一个时间过程,而不是一个时间点。其实真正的 Cancellation Points 只是在这些函数中 Cancellation Type 被修改为 PHREAD_CANCEL_ASYNCHRONOUS 和修改回 PTHREAD_CANCEL_DEFERRED 中间的一段时间。

POSIX 的取消类型有两种,一种是延迟取消(PTHREAD_CANCEL_DEFERRED),这是系统默认的取消类型,即在线程到达取消点之前,不会出现真正的取消;另外一种是异步取消(PHREAD_CANCEL_ASYNCHRONOUS),使用异步取消时,线程可以在任意时间取消。

4. Linux 的取消点实现

下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。

以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:

145      /* Enable asynchronous cancellation.  Required by the standard.  */
146      cbuffer.oldtype = __pthread_enable_asynccancel ();
147
148      /* Wait until woken by signal or broadcast.  */
149      lll_futex_wait (&amp;cond-&gt;__data.__futex, futex_val);
150
151      /* Disable asynchronous cancellation.  */
152      __pthread_disable_asynccancel (cbuffer.oldtype);

我们可以看到,在线程进入等待之前,pthread_cond_wait 先将线程取消类型设置为异步取消(__pthread_enable_asynccancel),当线程被唤醒时,线程取消类型被修改回延迟取消 __pthread_disable_asynccancel 。

这就意味着,所有在 __pthread_enable_asynccancel 之前接收到的取消请求都会等待 __pthread_enable_asynccancel 执行之后进行处理,所有在 __pthread_disable_asynccancel 之前接收到的请求都会在 __pthread_disable_asynccancel 之前被处理,所以真正的 Cancellation Point 是在这两点之间的一段时间。

5. 对示例函数进入死锁的解释

当 main 函数中调用 pthread_cancel 前,thread0 已经进入了 pthread_cond_wait 函数并将自己列入等待条件的线程列表中(lll_futex_wait)。这个可以通过 GDB 在各个函数上设置断点来验证。

当 pthread_cancel 被调用时,tid[0] 线程仍在等待,取消请求发生在 __pthread_disable_asynccancel 前,所以会被立即响应。但是 pthread_cond_wait 为注册了一个线程清理程序(glibc-2.6/nptl/pthread_cond_wait.c):

126  /* Before we block we enable cancellation.  Therefore we have to
127     install a cancellation handler.  */
128  __pthread_cleanup_push (&amp;buffer, __condvar_cleanup, &amp;cbuffer);

那么这个线程清理程序 __condvar_cleanup 干了什么事情呢?我们可以注意到在它的实现最后(glibc-2.6/nptl/pthread_cond_wait.c):

85  /* Get the mutex before returning unless asynchronous cancellation
86     is in effect.  */
87  __pthread_mutex_cond_lock (cbuffer-&gt;mutex);
88}

哦,__condvar_cleanup 在最后将 mutex 重新锁上了。而这时候 thread1 还在休眠(sleep(10)),等它醒来时,mutex 将会永远被锁住,这就是为什么 thread1 陷入无休止的阻塞中。

6. 如何避免因此产生的死锁

由于线程清理函数 pthread_cleanup_push 使用的策略是先进后出(FILO),那么我们可以在 pthread_cond_wait 函数前先注册一个线程处理函数:

void cleanup(void *arg)
{
  pthread_mutex_unlock(&amp;mutex);
}
void* thread0(void* arg)
{
  pthread_cleanup_push(cleanup, NULL);  // thread cleanup handler
  pthread_mutex_lock(&amp;mutex);
  pthread_cond_wait(&amp;cond, &amp;mutex);
  pthread_mutex_unlock(&amp;mutex);
  pthread_cleanup_pop(0);
  pthread_exit(NULL);
}

这样,当线程被取消时,先执行 pthread_cond_wait 中注册的线程清理函数 __condvar_cleanup,将 mutex 锁上,再执行 thread0 中注册的线程处理函数 cleanup,将 mutex 解锁。这样就避免了死锁的发生。

7. 结论

多线程下的线程同步一直是一个让人很头痛的问题。POSIX 为了避免立即取消程序引起的资源占用问题而引入的 Cancellation Points 概念是一个非常好的设计,但是不合适的使用 pthread_cancel 仍然会引起线程同步的问题。了解 POSIX 线程取消点在 Linux 下的实现更有助于理解它的机制和有利于更好的应用这个机制。

8. 参考文献

[1] W. Richard Stevens, Stephen A. Rago: Advanced Programming in the UNIX Environment, 2nd Edition.
[2] Linux Manpage

APUE, A Great Book

这两周是选课试听期,还没有正式开始上课,所以有点空闲就翻了翻 UINX 环境高级编程(Advanced Programming in the UNIX Environment, 2e),看了七八章,发现这本书真的是无愧于“UNIX 编程圣经”的称号。书中对编程中可能遇到的问题讲解得非常系统和详细,尤其当看到自己以前遇到过问题的地方时,简直就有一种顿悟的感觉,就想感叹一句“哦,原来如此!”。

我平常在写程序时,遇到问题总是求助于 Google。对那些讲编程技巧的书向来不怎么感冒(尤其是中国人写的),总觉得那种书根本不适合花时间仔细看一遍。这种问题驱动式的学习方式固然在解决某一特定问题时显得快捷高效,但是也往往受限于一叶障目不见泰山的困境。在解决了某一问题之后,对其它同类问题没有足够关注,导致再遇到类似问题时仍需要去搜索答案。

问题驱动式的学习方式会导致对问题的了解不够系统和深入,但如果仅仅拿本大部头慢慢翻完的话,又会枯燥无味,而且体会也不深。我觉得读编程书的最好方法就是,先有一定量的实践,再去看书,而且要保持对书中习题和代码的练习量。有时候不妨先看实例代码再看正文解释,如果代码看得懂,看作者的解释是否和自己理解一样;如果代码看不懂,就会加深对正文的注意度。而且有时候读那些入门级的教科书,不妨只看代码。

当然,在编程的时候,桌子上应该有几本经典图书当作手册来参考,不时地重读一下某些章节会很有好处。像 APUE,我就觉得非常适合作为案头书,做 Linux/Unix 开发的程序员买一本看看绝对不会失望。

Hello World 背后的真实故事

* 原作者:Antônio Augusto M. Fröhlich
* 原文链接http://www.lisha.ufsc.br/~guto/teaching/os/exercise/hello.html

* 译者:杨文博 <http://blog.solrex.org>
* 译文链接http://share.solrex.org/os/hello_cn.html
* 最后更新时间: 2008 年 2 月 28 日

我们计算机科学专业的大多数学生至少都接触过一回著名的 "Hello World" 程序。相比一个典型的应用程序——几乎总是有一个带网络连接的图形用户界面,"Hello World" 程序看起来只是一段很简单无趣的代码。不过,许多计算机科学专业的学生其实并不了解它背后的真实故事。这个练习的目的就是利用对 "Hello World" 的生存周期的分析来帮助你揭开它神秘的面纱。

源代码

让我们先看一下 Hello World 的源代码:

1. #include <stdio.h>
2. int main(void)
3. {
4. printf("Hello World!\n");
5. return 0;
6.
7. }

第 1 行指示编译器去包含调用 C 语言库(libc)函数 printf 所需要的头文件声明。

第 3 行声明了 main 函数,看起来好像是我们程序的入口点(在后面我们将看到,其实它不是)。它被声明为一个不带参数(我们这里不准备理会命令行参数)且会返回一个整型值给它的父进程(在我们的例子里是 shell)的函数。顺便说一下,shell 在调用程序时对其返回值有个约定:子进程在结束时必须返回一个 8 比特数来代表它的状态:0 代表正常结束,0~128 中间的数代表进程检测到的异常终止,大于 128 的数值代表由信号引起的终止。

从第 4 行到第 8 行构成了 main 函数的实现,即调用 C 语言库函数 printf 输出 "Hello World!\n" 字符串,在结束时返回 0 给它的父进程。

简单,非常简单!

编译

现在让我们看看 "Hello World" 的编译过程。在下面的讨论中,我们将使用非常流行的 GNU 编译器(gcc)和它的二进制辅助工具(binutils)。我们可以使用下面命令来编译我们的程序:

# gcc -Os -c hello.c

这样就生成了目标文件 hello.o,来看一下它的属性:

# file hello.o
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

给出的信息告诉我们 hello.o 是个可重定位的目标文件(relocatable),为 IA-32(Intel Architecture 32) 平台编译(在这个练习中我使用了一台标准 PC),保存为 ELF(Executable and Linking Format) 文件格式,并且包含着符号表(not stripped)。

顺便:

# objdump -hrt hello.o
hello.o: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000011 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000048 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000048 2**2
ALLOC
3 .rodata.str1.1 0000000d 00000000 00000000 00000048 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000033 00000000 00000000 00000055 2**0
CONTENTS, READONLY

SYMBOL TABLE:
00000000 l df *ABS* 00000000 hello.c
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l d .rodata.str1.1 00000000
00000000 l d .comment 00000000
00000000 g F .text 00000011 main
00000000 *UND* 00000000 puts

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000004 R_386_32 .rodata.str1.1
00000009 R_386_PC32 puts

这告诉我们 hello.o 有 5 个段:

(译者注:在下面的解释中读者要分清什么是 ELF 文件中的段(section)和进程中的段(segment)。比如 .text 是 ELF 文件中的段名,当程序被加载到内存中之后,.text 段构成了程序的可执行代码段。其实有时候在中文环境下也称 .text 段为代码段,要根据上下文分清它代表的意思。)

1. .text: 这是 "Hello World" 编译生成的可执行代码,也就是说这个程序对应的 IA-32 指令序列。.text 段将被加载程序用来初始化进程的代码段。

2. .data:"Hello World" 的程序里既没有初始化的全局变量也没有初始化的静态局部变量,所以这个段是空的。否则,这个段应该包含变量的初始值,运行前被装载到进程的数据段。

3. .bss: "Hello World" 也没有任何未初始化的全局或者局部变量,所以这个段也是空的。否则,这个段指示的是,在进程的数据段中除了上文的 .data 段内容,还有多少字节应该被分配并赋 0。

4. .rodata: 这个段包含着被标记为只读 "Hello World!\n" 字符串。很多操作系统并不支持进程(运行的程序)有只读数据段,所以 .rodata 段的内容既可以被装载到进程的代码段(因为它是只读的),也可以被装载到进程的数据段(因为它是数据)。因为编译器并不知道你的操作系统所使用的策略,所以它额外生成了一个 ELF 文件段。

5. .comment:这个段包含着 33 字节的注释。因为我们在代码中没有写任何注释,所以我们无法追溯它的来源。不过我们将很快在下面看到它是怎么来的。

它也给我们展示了一个符号表(symbol table),其中符号 main 的地址被设置为 00000000,符号 puts 未定义。此外,重定位表(relocation table)告诉我们怎么样去在 .text 段中去重定位对其它段内容的引用。第一个可重定位的符号对应于 .rodata 中的 "Hello World!\n" 字符串,第二个可重定位符号 puts,代表了使用 printf 所产生的对一个 libc 库函数的调用。为了更好的理解 hello.o 的内容,让我们来看看它的汇编代码:

1. # gcc -Os -S hello.c -o -
2. .file "hello.c"
3. .section .rodata.str1.1,"aMS",@progbits,1
4. .LC0:
5. .string "Hello World!"
6. .text
7. .align 2
8. .globl main
9. .type main,@function
10. main:
11. pushl %ebp
12. movl %esp, %ebp
13. pushl $.LC0
14. call puts
15. xorl %eax, %eax
16. leave
17. ret
18. .Lfe1:
19. .size n,.Lfe1-n
20. .ident "GCC: (GNU) 3.2 20020903 (Red Hat Linux 8.0 3.2-7)"

从汇编代码中我们可以清楚的看到 ELF 段标记是怎么来的。比如,.text 段是 32 位对齐的(第 7 行)。它也揭示了 .comment 段是从哪儿来的(第 20 行)。因为我们使用 printf 来打印一个字符串,并且我们要求我们优秀的编译器对生成的代码进行优化(-Os),编译器用(应该更快的) puts 调用来取代 printf 调用。不幸的是,我们后面将会看到我们的 libc 库的实现会使这种优化变得没什么用。

那么这段汇编代码会生成什么代码呢?没什么意外之处:使用标志字符串地址的标号 .LCO 作为参数的一个对 puts 库函数的简单调用。

连接

下面让我们看一下 hello.o 转化为可执行文件的过程。可能会有人觉得用下面的命令就可以了:

# ld -o hello hello.o -lc
ld: warning: cannot find entry symbol _start; defaulting to 08048184

不过,那个警告是什么意思?尝试运行一下!

是的,hello 程序不工作。让我们回到那个警告:它告诉我们连接器(ld)不能找到我们程序的入口点 _start。不过 main 难道不是入口点吗?简短的来说,从程序员的角度来看 main 可能是一个 C 程序的入口点。但实际上,在调用 main 之前,一个进程已经执行了一大堆代码来“为可执行程序清理房间”。我们通常情况下从编译器或者操作系统提供者那里得到这些外壳程序(surrounding code,译者注:比如 CRT)。

下面让我们试试这个命令:

# ld -static -o hello -L`gcc -print-file-name=` /usr/lib/crt1.o /usr/lib/crti.o hello.o /usr/lib/crtn.o -lc -lgcc

现在我们可以得到一个真正的可执行文件了。使用静态连接(static linking)有两个原因:一,在这里我不想深入去讨论动态连接库(dynamic libraries)是怎么工作的;二,我想让你看看在我们库(libc 和 libgcc)的实现中,有多少不必要的代码将被添加到 "Hello World" 程序中。试一下这个命令:

# find hello.c hello.o hello -printf "%f\t%s\n"
hello.c 84
hello.o 788
hello 445506

你也可以尝试 "nm hello" 和 "objdump -d hello" 命令来得到什么东西被连接到了可执行文件中。

想了解动态连接的更多内容,请参考 Program Library HOWTO

装载和运行

在一个遵循 POSIX(Portable Operating System Interface) 标准的操作系统(OS)上,装载一个程序是由父进程发起 fork 系统调用来复制自己,然后刚生成的子进程发起 execve 系统调用来装载和执行要运行的程序组成的。无论何时你在 shell 中敲入一个外部命令,这个过程都会被实施。你可以使用 truss 或者 trace 命令来验证一下:

# strace -i hello > /dev/null
[????????] execve("./hello", ["hello"], [/* 46 vars */]) = 0
...
[08053d44] write(1, "Hello World!\n", 13) = 13
...
[0804e7ad] _exit(0) = ?

除了 execve 系统调用,上面的输出展示了打印函数 puts 中的 write 系统调用,和用 main 的返回值(0)作为参数的 exit 系统调用。

为了解 execve 实施的装载过程背后的细节,让我们看一下我们的 ELF 可执行文件:

# readelf -l hello
Elf file type is EXEC (Executable file)
Entry point 0x80480e0
There are 3 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x55dac 0x55dac R E 0x1000
LOAD 0x055dc0 0x0809edc0 0x0809edc0 0x01df4 0x03240 RW 0x1000
NOTE 0x000094 0x08048094 0x08048094 0x00020 0x00020 R 0x4

Section to Segment mapping:
Segment Sections...
00 .init .text .fini .rodata __libc_atexit __libc_subfreeres .note.ABI-tag
01 .data .eh_frame .got .bss
02 .note.ABI-tag

输出显示了 hello 的整体结构。第一个程序头对应于进程的代码段,它将从文件偏移 0x000000 处被装载到映射到进程地址空间的 0x08048000 地址的物理内存中(虚拟内存机制)。代码段共有 0x55dac 字节大小而且必须按页对齐(0x1000, page-aligned)。这个段将包含我们前面讨论过的 ELF 文件中的 .text 段和 .rodata 段的内容,再加上在连接过程中生成的附加的段。正如我们预期,它被标志为:只读(R)和可执行(X),不过禁止写(W)。

第二个程序头对应于进程的数据段。装载这个段到内存的方式和上面所提到的一样。不过,需要注意的是,这个段占用的文件大小是 0x01df4 字节,而在内存中它占用了 0x03240 字节。这个差异主要归功于 .bss 段,它在内存中只需要被赋 0,所以不用在文件中出现(译者注:文件中只需要知道它的起始地址和大小即可)。进程的数据段仍然需要按页对齐(0x1000, page-aligned)并且将包含 .data 和 .bss 段。它将被标识为可读写(RW)。第三个程序头是连接阶段产生的,和这里的讨论没有什么关系。

如果你有一个 proc 文件系统,当你得到 "Hello World" 时停止进程(提示: gdb,译者注:用 gdb 设置断点),你可以用下面的命令检查一下是不是如上所说:

# cat /proc/`ps -C hello -o pid=`/maps
08048000-0809e000 r-xp 00000000 03:06 479202 .../hello
0809e000-080a1000 rw-p 00055000 03:06 479202 .../hello
080a1000-080a3000 rwxp 00000000 00:00 0
bffff000-c0000000 rwxp 00000000 00:00 0

第一个映射的区域是这个进程的代码段,第二个和第三个构成了数据段(data + bss + heap),第四个区域在 ELF 文件中没有对应的内容,是程序栈。更多和正在运行的 hello 进程有关的信息可以用 GNU 程序:time, ps 和 /proc/pid/stat 得到。

程序终止

当 "Hello World" 程序运行到 main 函数中的 return 语句时,它向我们在段连接部分讨论过的外壳函数传入了一个参数。这些函数中的某一个发起 exit 系统调用。这个 exit 系统调用将返回值转交给被 wait 系统调用阻塞的父进程。此外,它还要对终止的进程进行清理,将其占用的资源还给操作系统。用下面命令我们可以追踪到部分过程:

# strace -e trace=process -f sh -c "hello; echo $?" > /dev/null
execve("/bin/sh", ["sh", "-c", "hello; echo 0"], [/* 46 vars */]) = 0
fork() = 8321
[pid 8320] wait4(-1, <unfinished ...>
[pid 8321] execve("./hello", ["hello"], [/* 46 vars */]) = 0
[pid 8321] _exit(0) = ?
<... wait4 resumed> [WIFEXITED(s) && WEXITSTATUS(s) == 0], 0, NULL) = 8321
--- SIGCHLD (Child exited) ---
wait4(-1, 0xbffff06c, WNOHANG, NULL) = -1 ECHILD (No child processes)
_exit(0)

结束

这个练习的目的是让计算机专业的新生注意这样一个事实:一个 Java Applet 的运行并不是像魔法一样(无中生有的),即使在最简单的程序背后也有很多系统软件的支撑。如果您觉得这篇文章有用并且想提供建议来改进它,请发电子邮件给我

常见问题

这一节是为了回答学生们的常见问题。

* 什么是 "libgcc"? 为什么它在连接的时候被包含进来?
编译器内部的函数库,比如 libgcc,是用来实现目标平台没有直接实现的语言元素。举个例子,C 语言的模运算符 ("%") 在某个平台上可能无法映射到一条汇编指令。可能用一个函数调用实现比让编译器为其生成内嵌代码更受欢迎(特别是对一些内存受限的计算机来说,比如微控制器)。很多其它的基本运算,包括除法、乘法、字符串处理(比如 memory copy)一般都会在这类函数库中实现。

在 Ubuntu 上部署 Snort 入侵检测系统

最后更新时间:2009年7月1日

摘要:

这份文档主要描述了我在 Ubuntu 7.10 上安装部署 Snort 入侵检测系统和 acid 基于 PHP 的网页入侵检测数据库分析控制台的过程。

目录

1. 介绍
2. 安装过程
3. 总结
4. 参考文章

1. 介绍

Snort 是一款非常优秀的开源主机入侵检测系统软件,可以用来对主机的网络状况进行记录、分析和报警,并且支持用户自定义规则库。Snort 在 Windows 平台和 Linux 平台上均可运行,详细介绍请访问 Snort 的官方网站:http://www.snort.org

Snort的默认记录是存放在 log 文本文件中,而为了观察监控方便起见,一般使用 acidbase 这个网页控制台来查看(好像 MySQL 的 phpmyadmin)。所以整个过程需要:安装 snort 和相应包;安装 LAMP(Linux, Apache, MySQL, PHP) 服务器;在MySQL数据库中建立好Snort数据库并配置 Snort 使其将 log 存放在 MySQL 数据库中;为基于 PHP 的入侵检测数据库分析控制台 (acidbase) 配置好数据库连接。

2. 安装过程

[安装LAMP,Snort和一些软件库]

由于 Ubuntu 是 Debian 系的 Linux,安装软件非常简单,而且 Ubuntu 在中国科技大学有镜像,在教育网和科技网下载速度非常快(2~6M/s),就省掉了出国下载安装包的麻烦,只需要一个命令即可在几十秒钟内安装好所有软件。这里使用 Ubuntu 默认命令行软件包管理器 apt 来进行安装。

$ sudo apt-get install libpcap0.8-dev libmysqlclient15-dev mysql-client-5.0 mysql-server-5.0 bison flex apache2 libapache2-mod-php5 php5-gd php5-mysql libphp-adodb php-pear pcregrep snort snort-rules-default

需要注意的是在安装 MySQL 数据库时会弹出设置 MySQL 根用户口令的界面,临时设置其为“test”。

[在 MySQL 数据库中为 Snort 建立数据库]

Ubuntu 软件仓库中有一个默认的软件包 snort-mysql 提供辅助功能,用软件包管理器下载安装这个软件包。

$ sudo apt-get install snort-mysql

安装好之后查看帮助文档:

$ less /usr/share/doc/snort-mysql/README-database.Debian

根据帮助文档中的指令,在 MySQL 中建立 Snort 的数据库用户和数据库。所使用的命令如下:

$ mysql –u root –p

在提示符处输入上面设置的口令 test

mysql> CREATE DATABASE snort;
mysql> grant CREATE, INSERT, SELECT, UPDATE on snort.* to snort@localhost;
mysql> grant CREATE, INSERT, SELECT, UPDATE on snort.* to snort;
mysql> SET PASSWORD FOR snort@localhost=PASSWORD('snort-db');
mysql> exit

以上命令的功能是在 MySQL 数据库中建立一个 snort 数据库,并建立一个 snort 用户来管理这个数据库,设置 snort 用户的口令为 snort-db。

然后根据 README-database.Debian 中的指示建立 snort 数据库的结构。

$ cd /usr/share/doc/snort-mysql
$ zcat create_mysql.gz | mysql -u snort -D snort -psnort-db

这样就为 snort 在 MySQL 中建立了数据库的结构,其中包括各个 snort 需要使用的表。

[设置 snort 把 log 文件输出到 MySQL 数据库中]

修改 Snort 的配置文件:/etc/snort/snort.conf

$ sudo vim /etc/snort/snort.conf

在配置文件中将 HOME_NET 有关项注释掉,然后将 HOME_NET 设置为本机 IP 所在网络,将 EXTERNAL_NET 相关项注释掉,设置其为非本机网络,如下所示:

#var HOME_NET any
var HOME_NET 192.168.0.0/16
#var EXTERNAL_NET any
var EXTERNAL_NET !$HOME_NET

将 output database 相关项注释掉,将日志输出设置到 MySQL 数据库中,如下所示:

output database: log, mysql, user=snort password=snort-db dbname=snort host=localhost
#output database: log, mysql

这样,snort 就不再向 /var/log/snort 目录下的文件写记录了,转而将记录存放在 MySQL 的snort数据库中。这时候可以测试一下 Snort 工作是否正常:

$ sudo snort -c /etc/snort/snort.conf

如果出现一个用 ASCII 字符画出的小猪,那么 Snort 工作就正常了,可以使用 Ctrl-C 退出;如果 Snort 异常退出,就需要查明以上配置的正确性了。

[测试 Web 服务器 Apache 和 PHP 是否工作正常]

配置 apache 的 php 模块,添加 msql 和 gd 的扩展。

$ sudo vim /etc/php5/apache2/php.ini
extension=msql.so
extension=gd.so

重新启动 apache

$ /etc/init.d/apache2 restart

在/var/www/目录下新建一个文本文件test.php

$ sudo vim /var/www/test.php

输入内容:

<?php
phpinfo();
?>

然后在浏览器中输入 http://localhost/test.php,如果配置正确的话,就会出现 PHP INFO 的经典界面,就标志着 LAMP 工作正常。

[安装和配置 acid-base]

安装 acid-base 很简单,使用 Ubuntu 软件包管理器下载安装即可:

$ sudo apt-get install acidbase

安装过程中需要输入 acidbase 选择使用的数据库,这里选 MySQL,根用户口令 test,和 acid-base 的口令(貌似也可以跳过不设置)。

将acidbase从安装目录中拷贝到www目录中,也可以直接在apache中建立一个虚拟目录指向安装目录,这里拷贝过来主要是为了安全性考虑。

sudo cp –R /usr/share/acidbase/ /var/www/

因为 acidbase 目录下的 base_conf.php 原本是一个符号链接指向 /etc/acidbase/ 下的base_conf.php,为了保证权限可控制,我们要删除这个链接并新建 base_conf.php 文件。

$ rm base_conf.php
$ touch base_conf.php

暂时将 /var/www/acidbase/ 目录权限改为所有人可写,主要是为了配置 acidbase 所用。

$ sudo chmod 757 acidbase/

现在就可以开始配置 acid-base 了,在浏览器地址栏中输入 http://localhost/acidbase,就会转入安装界面,然后就点击 continue 一步步地进行安装:

选择语言为 english,adodb 的路径为:/usr/share/php/adodb;选择数据库为 MySQL,数据库名为 snort,数据库主机为 localhost,数据库用户名为 snort 的口令为 snort-db;设置 acidbase 系统管理员用户名和口令,设置系统管理员用户名为 admin,口令为 test。然后一路继续下去,就能安装完成了。

安装完成后就可以进入登录界面,输入用户名和口令,进入 acidbase 系统。

这里需要将 acidbase 目录的权限改回去以确保安全性,然后在后台启动 snort,就表明 snort 入侵检测系统的安装完成并正常启动了:

$ sudo chmod 775 acidbase/
$ sudo snort -c /etc/snort/snort.conf -i eth0 –D

[检查入侵检测系统工作状况,更改入侵检测规则]

正常情况下在一个不安全的网络中,登录 acidbase 后一会儿就能发现网络攻击。如果没有发现网络攻击,可以添加更严格的规则使得正常的网络连接也可能被报攻击,以测试 Snort IDS 的工作正确性,比如在 /etc/snort/rules/web-misc.rules 的最后添加下面的话:

$ sudo vi /etc/snort/rules/web-misc.rules
alert tcp any :1024 -> $HTTP_SERVER 500:

这一行的意思是:对从任何地址小于 1024 端口向本机 500 以上端口发送的 tcp 数据包都报警。杀死 Snort 的后台进程并重新启动,就应该能检测到正常的包也被当作攻击了。

$ sudo kill `pgrep snort`
$ sudo snort –c /etc/snort/snort.conf –i eth0 -D

3. 总结

使用 Ubuntu 部署 Snort 入侵检测系统和网页控制台是相当容易的,因为 Ubuntu 提供了很方便的软件包安装功能,只是有时候定制性能太差,需要用户手动去寻找软件包的安装位置。

4. 参考文章
http://www.howtoforge.com/intrusion-detection-with-snort-mysql-apache2-on-ubuntu-7.10

使用最新 ALSA 驱动解决 Ubuntu Linux Intel 集成声卡问题

目前用户所抱怨的 Ubuntu 系列的声卡问题,基本上归结为几类:一,找不到声音设备;二,不发声;三,耳机和音箱同时发声;四、话筒没声。大部分这种问题都是由笔记本上 Intel 集成声卡驱动引起的,关于这个问题的具体描述和解决方案,可以查看下面两个页面:

BUG:
https://bugs.launchpad.net/ubuntu/+source/linux-source-2.6.22/+bug/131133

SOLUTION:
https://wiki.ubuntu.com/Gutsy_Intel_HD_Audio_Controller

其实大部分问题都可以通过自己动手编译安装最新 ALSA 驱动解决,解决方法上面两个链接中已经解释得很清楚了,我这里介绍一下我的思路:

第一,查看 ALSA 版本,如果最新,就不用重新安装了,仔细查看一下配置吧。

$ alsactl -v

如果打印出: alsactl version 1.0.20,那么 ALSA 已经是最新了。

第二,在 ALSA 官方网站 http://www.alsa-project.org 上,下载最新的 ALSA 驱动,怎么解压我就不说了吧。

$ wget ftp://ftp.alsa-project.org/pub/driver/alsa-driver-1.0.20.tar.bz2
$ wget ftp://ftp.alsa-project.org/pub/lib/alsa-lib-1.0.20.tar.bz2
$ wget ftp://ftp.alsa-project.org/pub/utils/alsa-utils-1.0.20.tar.bz2

第三,查看自己的内核版本和声卡解码芯片是否被支持。
查看支持的内核版本

$ less alsa-driver-1.0.15/SUPPORTED_KERNELS

查看自己声卡解码芯片(如果系统不能识别声卡,可能无法由下面两个查到,那么查看你电脑配置单吧)

$ tail -2 /proc/asound/oss/sndstat

$ head -1 /proc/asound/card0/codec#0

比如我的 DELL D630 就显示的是下面这个

Codec: SigmaTel STAC9205

在 alsa-driver-1.0.20/sound/Documentation/ALSA-Configuration.txt 中查找自己声卡解码芯片对应的 model 名字,比如我的 STAC9205 对应的就是:

STAC9205/9254
ref Reference board
dell-m42 Dell (unknown)
dell-m43 Dell Precision
dell-m44 Dell Inspiron

如果存在对应的 model,恭喜你可以继续安装了。

第四,准备好编译环境,安装 gcc, libc 等等工具,还需要下载 Linux header,这是编译 ALSA 驱动必须的。

$ sudo apt-get install build-essential linux-headers-2.x.xx-xx(你的内核版本)

第五,编译安装,一般的 ./configure, make, make install 流程。注意的是驱动编译时候需要 module 名字,就是第三步中找到的 modle 对应的 module 名字,去掉 snd-即可。比如我的 STAC9205 对应的 modle 是 dell-m44,对应的 module 是 snd-hda-intel,那么 configure 时候需要加上 --with-cards=hda-intel。编译安装时遇到问题请仔细阅读源码附带的 INSTALL。

$ cd alsa-driver-1.0.15
$ ./configure --with-cards=MODULE_NAME
$ make
$ sudo make install
$ cd ../alsa-lib-1.0.15
$ ./configure
$ make
$ sudo make install
$ cd ../alsa-utils-1.0.15
$ ./configure
$ make
$ sudo make install

第六,配置 ALSA。
如果以前系统中没有使用 ALSA 驱动,可能需要 alsaconf 来配置,还要将 ALSA 加入内核模块中,我没有经验;
如果系统原来已经有 ALSA 驱动,但是没有识别声卡或者声音驱动有问题。那么理论上只需要在两个地方做改动:

添加 /etc/modprobe.d/sound 文件,内容为:

alias snd-card-0 snd-MODULE_NAME
alias snd-slot-0 snd-MODULE_NAME

比如我的就是:

alias snd-card-0 snd-hda-intel
alias sound-slot-0 snd-hda-intel

在 /etc/modprobe.d/alsa-base 中最后一行添加:

options snd-MODULE_NAME model=MODEL_NAME

比如我的就是:

options snd-hda-intel model=dell-m44

第七,重新启动查看效果,可以使用 alsamixer 调节声音设置。

其它都好,话筒没声
如果您已经安装了 alsa 驱动,话筒没声的问题可以通过设置解决。命令行输入 alsamixer:
1. Digital 那列,设置为 Analog I。
2. 点击 tab 制表键,在第一个 Capture 上点空格,显示出来红色的 "Capture, LR"字样表示选中,并把音量设置为 0。
3. Digital 那列,将音量设置为 67,这样能减少杂音。

由于各种各样的问题,比如 ALSA 驱动重复安装,内核版本,内核模块添加不全等等,本文并不保证能完美解决声卡问题。本人不是声卡驱动专家,本文只是阐述我解决声卡问题的做法,如果您在本文指导下仍然不能解决声卡问题,请您到熟悉的论坛上寻求帮助。请尽量不要发邮件给我,谢谢。

Ubuntu 7.10 的一些小问题

这两天用 Ubuntu 7.10 和 Compiz-Fusion 遇到了几个小问题:

1. Firefox 相关

Ubuntu 自带的 Firefox 2.0.0.6 好像有很多问题,用起来总不舒服。很诡异的一点是,如果启用默认的 Ubuntu Extension for Firefox,无法打开 Windows Live Space 的博客页面。我起初还以为是 Feedburner 的统计图片无法显示造成的假死呢,但在卸载 Ubuntu Extension for Firefox 后一切正常,这就让人搞不明白了 Ubuntu team 做出来这个东西究竟是想干吗?

2. Nvidia 显卡相关

我使用的驱动是 Nvidia 官方最新的 for linux 驱动,单屏是没有任何问题的。但当使用双屏时,就是 TwinView,笔记本是 1280x800 的分辨率,外接 LCD 是 1280x1024 的分辨率,这时候笔记本屏幕会使用外接 LCD 的高度,造成的效果是在笔记本屏幕上看不到工具栏,因为工具栏在底部...所以只能用 Alt+Tab 切窗口。

2. Compiz-Fusion 相关

开启了 Compiz-Fusion 3D 特效之后,由于吸附作用,窗口的标题栏会不正常,经常会跑到桌面面板工具栏的后面,或者说不停的在尝试吸附,表现就是标题栏闪屏。

在使用双屏时,第一次桌面立方体旋转需要很长时间,像死机了一样,我经常先切到 console 再重新切回 X window,但第一次旋转以后的旋转就很正常了,和单屏没什么区别,就是两个独立的屏一起转。

另外,双屏还是很耗资源的,虽然我是 1.8G 双核 CPU, 1G 内存, 256M 显卡,在跑双屏时应用程序反应会稍微慢一点儿,不过并不很明显,还是在可以忍受的范围之内。感觉上在 Linux 上使用双屏没有在 Windows 上快,不过,嘿嘿,Windows 双屏只能算两个桌面,我的 Ubuntu 双屏可是 8 个桌面!!!

北京马拉松 2007 和 QQ

早上来上班的路上发现知春路上多了很多人,每个路口都有站得笔直的警察和军人,偶尔还有几个保安,沿路二三十米一个带着治安巡逻红袖标的老大妈。本来我还为自己闯红灯惴惴,但想了一下查交通违规用不着这么大场面吧!到公司后马上到很少访问的新浪网上看了看新闻,哦,原来知春路是 2007 年北京马拉松的经过路段。反正是首都,总有这样那样的事情,比其它城市热闹多了。

最近有些厌学情绪,我不知道是因为前几天熬夜太多了精神不好还是因为天气转凉要有个适应期。虽然没有学多少,但也没有闲着,我基本连静静思考问题的时间都没有。前两天还抽空装了一下 Ubuntu Linux,在很新的硬件上安装 Linux 果然是一种很痛苦的事情,我忽然怀念起自己原来台式机落后的 Gforce MX440 显卡了。虽然安装了 Ubuntu 7.10,不过我还没有怎么用,这几天回去以后还是 Windows 用的比较多。

虽然我喜欢给别人宣扬开源的理念,可我从来不是一个彻底的 Linux 用户,从未把 Windows 从自己电脑上删除(曾经考虑过,但没实施),因为 Windows 也有很多让我不得不用的理由:我的 Samsung 手机数据传输,只有 Win 驱动;更包括很多 Office 文件,超星文件的使用问题;在中国的环境下,Linux 还没有适合中国用户的好的音乐播放器,好的下载软件,还有,现在对我来说很重要的一点是:好的语音聊天软件。不要和我提 Skype,在中科院(科学网)加南京大学(教育网+代理服务器)的条件下,还没有任何语音聊天软件比 QQ 更稳定(QQ 也不是很稳定)。所以我的 QQ 签名档是:“如果我QQ在线,说明正在和女朋友聊天,请勿打扰。闲时请使用 Gtalk 和我联系。”这是说真的,我只有在和女朋友语音聊天时候才会上 QQ,我们文字聊天也是使用 Gtalk 的。所以求求您了别对我的 QQ 签名档感到很好奇专门为此点开和我聊几句,请勿打扰就是请勿打扰,除非您觉得和我的关系非同一般。

讲到 QQ,现在恐怕很多人都会想起“珊瑚虫”的案子,我对那个倒不是很在意,侵犯知识产权就是侵犯知识产权,不要扯上什么企业道德,总之侵权是犯法的,受害者就有权告,受害者告不告和要求怎样,和侵权人的行为性质没有关系。我想说的是最近发现 QQ 也在悄悄改变,包括:可以使用“自定义状态”,像 MSN 和 Gtalk 的状态一样,会覆盖原始签名档;可以把 QQ 号与一个 Email 帐户绑定,然后对别人而言在所有显示 QQ 号码的地方都变成了 Email;据说 QQ Hummer 蜂鸟计划中有 Linux 版,希望能提供语音聊天功能,那样我又能减去一条不得不用 Win 的原因。

最近我的脑子里面没有一点安安静静的想法,所以,生活记录到这里吧。

在 Dell Latitude D630 上安装 Ubuntu LInux 7.10

摘要

这份文档主要描述了我在自己的 Dell Latitude D630 上安装 7.10 的过程,Ubuntu 7.10 现在在我的笔记本电脑上运行正常(准确说基本功能工作正常,因为我未做太多测试)。

目录

1. 介绍
2. 安装
3. 总结
4. 计算机硬件信息
5. 参考文章

1. 介绍

我最近买了一台 Dell Latitude D630 笔记本电脑。除了原装的 Windows XP Home Edition, 我已经在这台笔记本上安装 RHEL5,然后又安装了 Ubuntu 7.10 Linux 操作系统,下面是我安装 Gutsy Gibbon(Ubuntu 7.10) 的过程介绍。

2. 安装

[ 安装基本操作系统 ]
# wget http://releases.ubuntu.com/7.10/ubuntu-7.10-alternate-i386.iso
# wget http://archive.ubuntu.com/ubuntu/dists/gutsy/main/installer-i386/current/images/hd-media/initrd.gz
# wget http://archive.ubuntu.com/ubuntu/dists/gutsy/main/installer-i386/current/images/hd-media/vmlinuz
# cp vmlinuz initrd.gz /media/sda5
# echo "title Gutsy Install Entry" >> /boot/grub/menu.lst
# echo " root (hd0,4)" >> /boot/grub/menu.lst
# echo "kernel /vmlinuz root=/dev/ram ramdisk_size=40000 devfs=mount,dall" >> /boot/grub/menu.lst
# echo "initrd /initrd.gz" >> /boot/grub/menu.lst
# reboot
以上下载地址可以换成任何 Ubuntu 镜像站点的下载地址。重新启动时在 Grub 的启动列表中选择 Gusty Install Entry,接下来就是标准的安装过程了。关于里面硬盘分区的位置问题(即 (hd0,4), sda5) 等这些名字,每个计算机有所不同,请根据自己计算机分区和硬盘情况相应修改。最简单的办法,把那两个文件放在某个分区根目录下,一个分区一个分区的试。

[ 修改更新源列表 ]
添加以下行到 /etc/apt/source.list 中:
deb http://ubuntu.cn99.com/ubuntu/ gutsy main restricted
deb http://ubuntu.cn99.com/ubuntu/ gutsy-updates main restricted
deb http://ubuntu.cn99.com/ubuntu/ gutsy universe
deb http://ubuntu.cn99.com/ubuntu/ gutsy-updates universe
deb http://ubuntu.cn99.com/ubuntu/ gutsy multiverse
deb http://ubuntu.cn99.com/ubuntu/ gutsy-updates multiverse
deb http://ubuntu.cn99.com/ubuntu/ gutsy-security multiverse
deb http://ubuntu.cn99.com/ubuntu/ gutsy-security main restricted
deb http://ubuntu.cn99.com/ubuntu/ gutsy-security universe

在其它行前加上注释符 #

[ 更新操作系统,安装工具软件包 ]
$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install libc6 libc6-dev
$ sudo apt-get install gcc-4.2
$ sudo apt-get install g++-4.2
libc6 和 gcc 主要是为下面安装显卡驱动准备的。

[ 下载 nVidia 最新显卡驱动 ]
$ wget http://us.download.nvidia.com/XFree86/Linux-x86/100.14.09/NVIDIA-Linux-x86-100.14.09-pkg1.run

[ 修改 root 用户密码 ]
$ sudo passwd

[ 安装显卡驱动 ]
$ sudo reboot
重新启动时在 Grub 的启动列表中选择 Ubuntu 7.10, kernel 2.6.22-14-generic (recovery mode),输入 root 用户密码,运行 NVIDIA-Linux-x86-100.14.09-pkg1.run,按照指示进行。安装成功后重新启动系统,在系统启动前跳出的对话框里选择配置显示效果,选择 1280x800 分辨率的基本配置,并在下方的使用宽屏选项栏打上勾。

[ 解决 DVD±RW 驱动问题 ]
$ cp /etc/modules /etc/modules.bak
$ cp /etc/modules /tmp
$ echo piix >> /tmp/modules
$ sudo cp /tmp/modules /etc/modules
$ sudo reboot
这个我未曾尝试,因为还没有在 Linux 下刻录过光盘。

[ 解决声卡驱动问题 ]
因为 Dell Latitude D630 声卡版本比较新,用 Ubuntu 自带的 ALSA 驱动无法配置。解决办法是手工安装 ALSA 最新版本的 driver, lib 和 utils。详细步骤请参考:
http://blog.solrex.org/articles/solve-ubuntu-intel-soundcard-problem-with-latest-alsa-driver.html

[ 安装中文输入法 ]
首先在 System->Administration->Language Support 中选择安装中文支持,自动安装完中文支持以后:
$ sudo apt-get install scim-bridge-agent
$ sudo im-switch -s scim -z default
在 /etc/X11/xinit/xinput.d/scim 中找到以下两行并更改:
GTK_IM_MODULE=scim-bridge
QT_IM_MODULE=xim
如果 im-switch 不成功,可以手动将设置文件 copy 到 default 去
$ sudo cp /etc/X11/xinit/xinput.d/default /etc/X11/xinit/xinput.d/default.bak
$ sudo cp /etc/X11/xinit/xinput.d/scim /etc/X11/xinit/xinput.d/default
$ sudo reboot
如果重启后 Firefox 仍然不可以使用中文输入,可以在 Firefox 的启动脚本 /usr/lib/firefox/run-mozilla.sh 中最后 exit 前面加这么一句:
export GTK_IM_MODULE=xim

[ 安装 Windows 中文字体 ]
未在电脑上安装 Windows 操作系统者不需要此步骤,已安装者也可以选择跳过。我写了一个脚本做这个工作,可以下面链接下载。

$ wget http://share.solrex.org/scripts/install_win_CN_fonts_to_linux.sh
$ sudo sh install_win_CN_fonts_to_linux.sh
重新登录 X window

[ 安装 compiz-fusion 3D 桌面效果 ]
为了这一步的安装,你需要修改一下 xorg.conf 文件,请参考:http://compiz.org/NVidia。需要特别注意的是,如果选择了官方驱动,请不要安装任何 Ubuntu 软件仓库里的驱动。你可以从下面得到我的 xorg.conf 文件。
$ wget http://share.solrex.org/scripts/xorg.conf
$ sudo apt-get install compiz-fusion-plugins-* compizconfig-settings-manager
安装完成以后在 System->Preference->Appearance 中的 Visual Effects 中多出一项 Custom,选择以后,点右侧的 Preferences 配置效果。
注:最新版本的 Ubuntu 官方驱动已经支持 D630 的显卡,在选择 Custom选项时会自动安装驱动。安装完驱动后自动配置的 xorg.conf文件为:
http://share.solrex.org/scripts/xorg.conf.glx 

3. 总结

硬件信息 Linux 下状态 备注
Intel Core2 Duo T7100 @ 1.8 GHz 工作正常
14.1" WXGA TFT (1280x800) 工作正常
nVidia Corporation Quadro NVS 135M 工作正常 手工安装驱动程序
1 GB RAM (2 x 512 MB DDR2 533 MHz) 工作正常
120 GB HDD (Hitachi HTS722016K9A300) 工作正常
DVD±RW (PBDS DVD±RW DS-8W1P) 未验证 据说可以在 /etc/modules 增加 piix 解决.
Broadcom NetXtreme BCM5755M Gigabit Ethernet 工作正常
Broadcom Corporation BCM94311MCG wlan mini-PCI 未验证
Intel 82801H sound card (ICH8) 工作正常 Ubuntu 7.10 参见上面讲的处理方法.
Memory card reader 未验证
Internal 56k modem 未验证

4. 计算机信息

[ 所有 pci 设备 ]
$ lspci
00:00.0 Host bridge: Intel Corporation Mobile PM965/GM965/GL960 Memory Controller Hub (rev 0c)
00:01.0 PCI bridge: Intel Corporation Mobile PM965/GM965/GL960 PCI Express Root Port (rev 0c)
00:1a.0 USB Controller: Intel Corporation 82801H (ICH8 Family) USB UHCI Contoller #4 (rev 02)
00:1a.1 USB Controller: Intel Corporation 82801H (ICH8 Family) USB UHCI Controller #5 (rev 02)
00:1a.7 USB Controller: Intel Corporation 82801H (ICH8 Family) USB2 EHCI Controller #2 (rev 02)
00:1b.0 Audio device: Intel Corporation 82801H (ICH8 Family) HD Audio Controller (rev 02)
00:1c.0 PCI bridge: Intel Corporation 82801H (ICH8 Family) PCI Express Port 1 (rev 02)
00:1c.1 PCI bridge: Intel Corporation 82801H (ICH8 Family) PCI Express Port 2 (rev 02)
00:1c.5 PCI bridge: Intel Corporation 82801H (ICH8 Family) PCI Express Port 6 (rev 02)
00:1d.0 USB Controller: Intel Corporation 82801H (ICH8 Family) USB UHCI Controller #1 (rev 02)
00:1d.1 USB Controller: Intel Corporation 82801H (ICH8 Family) USB UHCI Controller #2 (rev 02)
00:1d.2 USB Controller: Intel Corporation 82801H (ICH8 Family) USB UHCI Controller #3 (rev 02)
00:1d.7 USB Controller: Intel Corporation 82801H (ICH8 Family) USB2 EHCI Controller #1 (rev 02)
00:1e.0 PCI bridge: Intel Corporation 82801 Mobile PCI Bridge (rev f2)
00:1f.0 ISA bridge: Intel Corporation 82801HEM (ICH8M) LPC Interface Controller (rev 02)
00:1f.1 IDE interface: Intel Corporation 82801HBM/HEM (ICH8M/ICH8M-E) IDE Controller (rev 02)
00:1f.2 IDE interface: Intel Corporation 82801HBM/HEM (ICH8M/ICH8M-E) SATA IDE Controller (rev 02)
00:1f.3 SMBus: Intel Corporation 82801H (ICH8 Family) SMBus Controller (rev 02)
01:00.0 VGA compatible controller: nVidia Corporation Quadro NVS 135M (rev a1)
03:01.0 CardBus bridge: O2 Micro, Inc. Cardbus bridge (rev 21)
03:01.4 FireWire (IEEE 1394): O2 Micro, Inc. Firewire (IEEE 1394) (rev 02)
09:00.0 Ethernet controller: Broadcom Corporation NetXtreme BCM5755M Gigabit Ethernet PCI Express (rev 02)
0c:00.0 Network controller: Broadcom Corporation BCM94311MCG wlan mini-PCI (rev 01)

以下省略了,只是写一下查看这些信息的命令
[ 电池信息 ]
$ less /proc/acpi/battery/BAT0/info
[ CPU 信息 ]
$ less /proc/cpuinfo
[ 内存信息 ]
$ less /proc/meminfo
[ 内核消息 ]
$ dmesg
[ 加载的内核模块 ]
$ lsmod
[ X.org 设置文件(显示及输入设置)]
$ less /etc/X11/xorg.conf

5. 参考文章

* HARDWARE Dell Latitude D630
* My Ubuntu Installation
* Ubuntu on my Dell Latitude D630

在双系统环境中使用Windows字体美化Linux中文字体

安装完 Linux 操作系统的中文支持后,看到那些难看的字体是不是很不爽,下面这个脚本就帮助你使用 Windows 字体来自动美化 Linux 中文字体。

下面的脚本使用 windows 自带字体美化 Linux 字体,主要是将软件中文字体使用宋体,终端使用黑体替换。脚本源程序可以在 http://share.solrex.org/scripts/install_win_CN_fonts_to_linux.sh 下载到。

首先确保 windows 系统所在分区被挂载到 /media 目录下将以下内容保存为脚本文件:install_win_CN_fonts_to_linux.sh

#!/bin/bash
check_answer()
{
    case $1 in
        y|Y|Yes|yes|YES)
            return 0;
            ;;
        n|N|No|no|NO)
            return 1;
            ;;
        *) echo "Answer either yes or no, default is yes"
            return 2
            ;;
    esac
}

LIN_FONTS_DIR=/usr/share/fonts/zh_CN/TrueType
LANG_SEL_FILE=/etc/fonts/language-selector.conf
SEARCH_DIR=/media

echo -n "This script is for installing Windows Chinese fonts to Linux on a dual-OS-installed computer. And the writer doesn't promise it will work on your system. Do you want to continue?(y/n)[y]:"
read ANS_STR
: ${ANS_STR:="y"}
check_answer $ANS_STR
ANS=$?
if [ $ANS -ne 0 ]; then
    echo "Exit."
    exit
fi

echo -n "There might be license problems, i.e. you have no permission to use Windows fonts in other program except Windows OS. Do you want to continue?(y/n)[y]:"
read ANS_STR
: ${ANS_STR:="y"}
check_answer $ANS_STR
ANS=$?
if [ $ANS -ne 0 ]; then
    echo "Exit."
    exit
fi

echo -n "You run this script with root account or "sudo" command?(y/n)[y]:"
read ANS_STR
: ${ANS_STR:="y"}
check_answer $ANS_STR
ANS=$?
if [ $ANS -ne 0 ]; then
    echo "Exit."
    exit
fi

echo -n "You mounted Windows System partation "C:\" to /media directory?(y/n)[y]:"
read ANS_STR
: ${ANS_STR:="y"}
check_answer $ANS_STR
ANS=$?
if [ $ANS -ne 0 ]; then
    echo "Exit."
    exit
fi

echo "Searching for windows's fonts directory..."
echo "That may take a few minutes, please wait."
WIN_FONTS_DIR=`echo $(find $SEARCH_DIR -name simsun.ttc -type f) | sed s/simsun.ttc//g`
if [ -d $WIN_FONTS_DIR ]; then
    while true; do
        echo "Windows fonts dir found! Your windows fonts directory is: "
        echo -n ""$WIN_FONTS_DIR" Is it right?(y/n)[y]:"
        read ANS_STR
        : ${ANS_STR:="y"}
        check_answer $ANS_STR
        ANS=$?
        if [ $ANS -eq 0 ]; then
            break
        elif [ $ANS -eq 1 ]; then 
            echo "Please enter path to your windows fonts dir?"
            read ANS_STR
            if [ -d $ANS_STR ]; then
                WIN_FONTS_DIR=$ANS_STR
                break
            else
                echo "Your input is not a dir. Exit."
                exit
            fi
        else
            echo "Unrecognized input."
        fi
    done
else
    echo "Can not find windows fonts directory. "
    echo -n "Please enter path to your windows fonts dir:"
    read ANS_STR
    if [ -d $ANS_STR ]; then
        WIN_FONTS_DIR=$ANS_STR
    else
        echo "Your input is not a dir. Exit."
        exit
    fi
fi

echo "Entering $WIN_FONTS_DIR"
cd $WIN_FONTS_DIR

echo "Creating Linux fonts directory: $LIN_FONTS_DIR..."
mkdir -p $LIN_FONTS_DIR
if [ $? -ne 0 ]; then
    echo "Creating Linux fonts dir failed. Exit."
    exit
fi

echo "Copying windows fonts to linux fonts dir... sim*|tahoma|tohomabd.ttf"
cp sim* SimSun18030.ttc tahoma.ttf tahomabd.ttf $LIN_FONTS_DIR
if [ $? -ne 0 ]; then
    echo "Copying windows fonts to linux fonts dir failed. Exit."
    exit
fi

echo "Changing font files' access permissions.."
chmod 644 $LIN_FONTS_DIR/*

echo "Entering $LIN_FONTS_DIR"
cd $LIN_FONTS_DIR

echo "Creating an index of scalable font files in $LIN_FONTS_DIR for X..."
mkfontscale
if [ $? -ne 0 ]; then
    echo "Creating an index of scalable font files for X failed. Exit."
    exit
fi

echo "Creating an index of X font files in $LIN_FONTS_DIR"
mkfontdir
if [ $? -ne 0 ]; then
    echo "Creating an index of X font files in $LIN_FONTS_DIR failed. Exit."
    exit
fi

echo "Creating an index of FreeType font files in $LIN_FONTS_DIR"
fc-cache $LIN_FONTS_DIR
if [ $? -ne 0 ]; then
    echo "Creating an index of FreeType font files in $LIN_FONTS_DIR failed. Exit."
    exit
fi

echo "Backing up $LANG_SEL_FILE"
if [ -f $LANG_SEL_FILE ]; then
    cp $LANG_SEL_FILE $LANG_SEL_FILE.bak
fi

echo "Generating new $LANG_SEL_FILE... for language select"
echo '
<fontconfig>
  <alias>
    <family>serif</family>
      <prefer>
        <family>Bitstream Vera Serif</family>
        <family>SimSun</family>
        <family>DejaVu Serif</family>
        <family>AR PL ShanHeiSun Uni</family>
        <family>AR PL ZenKai Uni</family>
      </prefer>
  </alias>
  <alias>
    <family>sans-serif</family>
      <prefer>
        <family>Bitstream Vera Sans</family>
        <family>SimSun</family>
        <family>DejaVu Sans</family>
        <family>AR PL ShanHeiSun Uni</family>
        <family>AR PL ZenKai Uni</family>
      </prefer>
  </alias>
  <alias>
    <family>monospace</family>
      <prefer>
        <family>DejaVu Sans Mono</family>
        <family>Bitstream Vera Sans Mono</family>
        <family>SimHei</family>
    </prefer>
  </alias>

<match target="font" >
  <test name="family" compare="contains" >
    <string>Song</string>
    <string>Sun</string>
    <string>Kai</string>
    <string>Ming</string>
  </test>
  <test compare="more_eq" target="pattern" name="weight" >
    <int>180</int>
  </test>
  <edit mode="assign" name="embolden" >
    <bool>true</bool>
  </edit>
</match>

<match target="font" >
  <test name="family" compare="contains" >
    <string>Song</string>
    <string>Sun</string>
    <string>Kai</string>
    <string>Ming</string>
  </test>
  <edit name="globaladvance">
    <bool>false</bool>
  </edit>
  <edit name="spacing">
    <int>0</int>
  </edit>
  <edit name="hinting">
    <bool>true</bool>
  </edit>
  <edit name="autohint">
    <bool>false</bool>
  </edit>
  <edit name="antialias" mode="assign">
    <bool>true</bool>
  </edit>
  <test name="pixelsize" compare="less_eq">
    <int>18</int>
  </test>
  <edit name="antialias" mode="assign" >
    <bool>false</bool>
  </edit>
</match>

<match target="pattern">
  <test name="family">
    <string>SimSun</string>
    <string>SimSun-18030</string>
    <string>AR PL ShanHeiSun Uni</string>
    <string>AR PL New Sung</string>
    <string>MingLiU</string>
    <string>PMingLiU</string>
  </test>
  <edit binding="strong" mode="prepend" name="family">
    <string>Tahoma</string>
    <string>Verdana</string>
  </edit>
</match>

<match target="pattern">
  <test name="family">
    <string>宋体</string>
  </test>
  <edit name="family" mode="assign">
    <string>SimSun</string>
  </edit>
</match>
<match target="pattern">
  <test name="family">
    <string>新宋体</string>
  </test>
  <edit name="family" mode="assign">
    <string>SimSun</string>
  </edit>
</match>
<match target="pattern">
  <test name="family">
    <string>仿宋_GB2312</string>
  </test>
  <edit name="family" mode="assign">
    <string>FangSong_GB2312</string>
  </edit>
</match>
<match target="pattern">
  <test name="family">
    <string>楷体_GB2312</string>
  </test>
  <edit name="family" mode="assign">
    <string>KaiTi_GB2312</string>
  </edit>
</match>
<match target="pattern">
  <test name="family">
    <string>黑体</string>
  </test>
  <edit name="family" mode="assign">
    <string>SimHei</string>
  </edit>
</match>
</fontconfig> ' > $LANG_SEL_FILE
echo "Finished. Please quit your X session and relogin."
echo "If you have problems on start X(sometimes happened on Ubuntu 7.04.), please delete $LIN_FONTS_DIR and copy the backuped $LANG_SEL_FILE.bak to $LANG_SEL_FILE. Then do "fc-cache" with no options. That may work."

执行:
sudo sh ./install_win_CN_fonts_to_linux.sh

登出 Xwindow,再重新登录即可。

Hacking 《自己动手写操作系统》Chapter 4

——Writing x86 PC Bootloader With Free Software 2

本文内容已被整理为一本电子书,请到这里下载

强烈建议随《自己动手写操作系统》这本书读本篇文章,基础知识:x86 汇编指令,AT&T 汇编格式,FAT12 文件系统格式。

由于《自己动手写操作系统》第三章讲的是保护模式,示例代码全是 DOS 可执行代码,和直接的 Bootloader 没有太大关系,所以这次继续下来从第四章开始。

前两章中写的 Bootloader 其实严格意义上只算是一个 Boot sector(扇区),因为它只是简单地打印了一个字符。第四章的 Bootloader 实现了从软盘中读写一个文件并加载运行的功能。

总共有以下几个文件:boot.S loader.S solrex_x86_boot.ld solrex_x86_dos.ld Makefile

下面解释每个文件的作用:
boot.S:
它生成的 .bin 文件就是软盘镜像的第一个扇区。由上一篇文章提供的 boot.S 只有启动功能,生成的软盘镜像文件只能作为启动器而不能作为可读写软盘来使用,因为在那个镜像里面没有将软盘格式化。其实格式化说白了就是在软盘的第一个扇区添加一些头信息,这样系统就知道如何处理这张软盘,在 boot.S 中的 Floppy header 就是做这个工作的。而在软盘的前 3 个 byte 是一句跳转指令,这样在启动时候系统会跳过 floppy header,也解决了如何启动的问题。这样这张软盘既可以作为启动盘,又可以作为文件盘使用了。因为启动扇区只有 512 个字节,不可能依赖它作为启动功能,这样我们可以将另一个程序 LOADER.BIN 放入软盘,启动后,从软盘读取它进入内存并执行。

所以 boot.S 代码的主要内容是,首先将软盘主目录复制到内存中,并在其中搜索 LOADER.BIN,搜索到以后,将 LOADER.BIN 加载入内存并执行。

loader.S:
就是一个普通的打印程序,在屏幕第一行中间打印一个 'L'

solrex_x86_boot.ld:
boot.S 的连接脚本,将 boot.S 代码段连接到 0x7c00 的位置(x86 PC 操作系统启动的位置)。

solrex_x86_dos.ld:
loader.S 的连接脚本,将 loader.S 代码段连接到 0x1000 的位置,和普通 DOS 程序一样。

Makefile:
不说了,大家都知道。

这个 hacking 系列我准备继续写下去,示例代码可以从我的个人主页 http://solrex.org 打包下载,包括《自己动手写操作系统》的部分源代码,以 .asm 为后缀。对 Intel(MASM,NASM,TASM) 和 AT&T(GAS)汇编语言语法在 x86 平台的实现如何转换感兴趣的朋友,可以对比阅读 .asm 和 .S 文件。

[solrex@NJU-CAS Solrex]$ more boot.S loader.S solrex_x86_boot.ld solrex_x86_dos.ld Makefile
::::::::::::::
boot.S
::::::::::::::
.code16
.set BaseOfStack, 0x7c00 /* Stack base address, inner */
.set BaseOfLoader, 0x9000 /* Section loading address of LOADER.BIN */
.set OffsetOfLoader, 0x0100 /* Loading offset of LOADER.BIN */
.set RootDirSectors, 14 /* Root directory sector count */
.set SecNoOfRootDir, 19 /* 1st sector of root directory */
.set SecNoOfFAT1, 1 /* 1st sector of FAT1 */
.set DeltaSecNo, 17 /* BPB_(RsvdSecCnt+NumFATs*FATSz) -2 */
/* Start sector of file space =*/
.text
/* Floppy header of FAT12 */
jmp LABEL_START /* Start to boot. */
nop /* nop required */
BS_OEMName: .ascii "WB. YANG" /* OEM String, 8 bytes required */
BPB_BytsPerSec: .2byte 512 /* Bytes per sector */
BPB_SecPerCluse: .byte 1 /* Sector per cluse */
BPB_ResvdSecCnt: .2byte 1 /* Reserved sector count */
BPB_NumFATs: .byte 2 /* Number of FATs */
BPB_RootEntCnt: .2byte 224 /* Root entries count */
BPB_TotSec16: .2byte 2880 /* Total sector number */
BPB_Media: .byte 0xf0 /* Media descriptor */
BPB_FATSz16: .2byte 9 /* FAT size(sectors) */
BPB_SecPerTrk: .2byte 18 /* Sector per track */
BPB_NumHeads: .2byte 2 /* Number of magnetic heads */
BPB_HiddSec: .4byte 0 /* Number of hidden sectors */
BPB_TotSec32: .4byte 0 /* If TotSec16 equal 0, this works */
BS_DrvNum: .byte 0 /* Driver number of interrupt 13 */
BS_Reserved1: .byte 0 /* Reserved */
BS_BootSig: .byte 0x29 /* Boot signal */
BS_VolID: .4byte 0 /* Volume ID */
BS_VolLab: .ascii "Solrex 0.01" /* Volume label, 11 bytes required */
BS_FileSysType: .ascii "FAT12 " /* File system type, 8 bytes required */

/* Initial registers. */
LABEL_START:
mov %cs,%ax
mov %ax,%ds
mov %ax,%es
mov %ax,%ss
mov $BaseOfStack, %sp

/* Clear screen */
mov $0x0600,%ax /* %ah=6, %al=0 */
mov $0x0700,%bx /* Black white */
mov $0,%cx /* Top left: (0,0) */
mov $0x184f,%dx /* Bottom right: (80,50) */
int $0x10 /* BIOS int 10h, ah=6: Initialize screen */

/* Display "Booting**" */
mov $0,%dh
call DispStr /* Display string(index 0)*/

/* Reset floppy */
xor %ah,%ah
xor %dl,%dl /* %dl=0: floppy driver 0 */
int $0x13 /* BIOS int 13h, ah=0: Reset driver 0 */

/* Find LOADER.BIN in root directory of driver 0 */
movw $SecNoOfRootDir, (wSectorNo)

/* Read root dir sector to memory */
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
cmpw $0,(wRootDirSizeForLoop) /* If searching in root dir */
jz LABEL_NO_LOADERBIN /* can find LOADER.BIN ? */
decw (wRootDirSizeForLoop)
mov $BaseOfLoader,%ax
mov %ax,%es /* %es <- BaseOfLoader*/
mov $OffsetOfLoader,%bx /* %bx <- OffsetOfLoader */
mov (wSectorNo),%ax /* %ax <- sector number in root */
mov $1,%cl
call ReadSector
mov $LoaderFileName,%si /* %ds:%si -> LOADER BIN */
mov $OffsetOfLoader,%di /* BaseOfLoader<<4+100*/
cld
mov $0x10,%dx

/* Search for "LOADER BIN", FAT12 save file name in 12 bytes, 8 bytes for
file name, 3 bytes for suffix, last 1 bytes for '20'. If file name is
less than 8 bytes, filled with '20'. So "LOADER.BIN" is saved as:
"LOADER BIN"(4f4c 4441 5245 2020 4942 204e).
*/
LABEL_SEARCH_FOR_LOADERBIN:
cmp $0,%dx /* Read control */
jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR
dec %dx
mov $11,%cx

LABEL_CMP_FILENAME:
cmp $0,%cx
jz LABEL_FILENAME_FOUND /* If 11 chars are all identical? */
dec %cx
lodsb /* %ds:(%si) -> %al*/
cmp %es:(%di),%al
jz LABEL_GO_ON
jmp LABEL_DIFFERENT /* Different */

LABEL_GO_ON:
inc %di
jmp LABEL_CMP_FILENAME /* Go on loop */

LABEL_DIFFERENT:
and $0xffe0,%di /* Go to head of this entry */
add $0x20,%di
mov $LoaderFileName,%si /* Next entry */
jmp LABEL_SEARCH_FOR_LOADERBIN

LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
addw $1,(wSectorNo)
jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN

/* Not found LOADER.BIN in root dir. */
LABEL_NO_LOADERBIN:
mov $2,%dh
call DispStr /* Display string(index 2) */
jmp . /* Infinite loop */

/* Found. */
LABEL_FILENAME_FOUND:
mov $RootDirSectors,%ax
and $0xffe0,%di /* Start of current entry, 32 bytes per entry */
add $0x1a,%di /* First sector of this file */
mov %es:(%di),%cx
push %cx /* Save index of this sector in FAT */
add %ax,%cx
add $DeltaSecNo,%cx /* LOADER.BIN's start sector saved in %cl */
mov $BaseOfLoader,%ax
mov %ax,%es /* %es <- BaseOfLoader */
mov $OffsetOfLoader,%bx /* %bx <- OffsetOfLoader */
mov %cx,%ax /* %ax <- Sector number */

/* Load LOADER.BIN's sector's to memory. */
LABEL_GOON_LOADING_FILE:
push %ax
push %bx
mov $0x0e,%ah
mov $'.',%al /* Char to print */
mov $0x0f,%bl /* Front color: white */
int $0x10 /* BIOS int 10h, ah=0xe: Print char */
pop %bx
pop %ax

mov $1,%cl
call ReadSector
pop %ax /* Got index of this sector in FAT */
call GetFATEntry
cmp $0x0fff,%ax
jz LABEL_FILE_LOADED
push %ax /* Save index of this sector in FAT */
mov $RootDirSectors,%dx
add %dx,%ax
add $DeltaSecNo,%ax
add (BPB_BytsPerSec),%bx
jmp LABEL_GOON_LOADING_FILE

LABEL_FILE_LOADED:
mov $1,%dh
call DispStr /* Display string(index 1) */

/*******************************************************************
Jump to LOADER.BIN's start address in memory.
*/
jmp $BaseOfLoader,$OffsetOfLoader
/*******************************************************************/

/* ==================================================================
Variable table
*/
wRootDirSizeForLoop: .2byte RootDirSectors
wSectorNo: .2byte 0 /* Sector number to read */
bOdd: .byte 0 /* odd or even? */

/* ==================================================================
String table
*/
LoaderFileName: .asciz "LOADER BIN" /* File name */
.set MessageLength,9
BootMessage: .ascii "Booting**" /* index 0 */
Message1: .ascii "Loaded in" /* index 1 */
Message2: .ascii "No LOADER" /* index 2 */

/* ==================================================================
Routine: DispStr
Action: Display a string, string index stored in %dh
*/
DispStr:
mov $MessageLength, %ax
mul %dh
add $BootMessage,%ax
mov %ax,%bp /* String address */
mov %ds,%ax
mov %ax,%es
mov $MessageLength,%cx /* String length */
mov $0x1301,%ax /* ah = 0x13, al = 0x01(W) */
mov $0x07,%bx /* PageNum 0(bh = 0), bw(bl= 0x07)*/
mov $0,%dl /* Start row and column */
int $0x10 /* BIOS INT 10h, display string */
ret

/* ==================================================================
Routine: ReadSector
Action: Read %cl Sectors From %ax sector(floppy) to %es:%bx(memory)
Assume sector number is 'x', then:
x/(BPB_SecPerTrk) = y,
x%(BPB_SecPerTrk) = z.
The remainder 'z' PLUS 1 is the start sector number;
The quotient 'y' devide by BPB_NumHeads(RIGHT SHIFT 1 bit)is cylinder
number;
AND 'y' by 1 can got magnetic header.
*/
ReadSector:
push %ebp
mov %esp,%ebp
sub $2,%esp /* Reserve space for saving %cl */
mov %cl,-2(%ebp)
push %bx /* Save bx */
mov (BPB_SecPerTrk), %bl /* %bl: the devider */
div %bl /* 'y' in %al, 'z' in %ah */
inc %ah /* z++, got start sector */
mov %ah,%cl /* %cl <- start sector number */
mov %al,%dh /* %dh <- 'y' */
shr $1,%al /* 'y'/BPB_NumHeads */
mov %al,%ch /* %ch <- Cylinder number(y>>1) */
and $1,%dh /* %dh <- Magnetic header(y&1) */
pop %bx /* Restore %bx */
/* Now, we got cylinder number in %ch, start sector number in %cl, magnetic
header in %dh. */
mov (BS_DrvNum), %dl
GoOnReading:
mov $2,%ah
mov -2(%ebp),%al /* Read %al sectors */
int $0x13
jc GoOnReading /* If CF set 1, mean read error, reread. */
add $2,%esp
pop %ebp
ret

/* ==================================================================
Routine: GetFATEntry
Action: Find %ax sector's index in FAT, save result in %ax
*/
GetFATEntry:
push %es
push %bx
push %ax
mov $BaseOfLoader,%ax
sub $0x0100,%ax
mov %ax,%es /* Left 4K bytes for FAT */
pop %ax
movb $0,(bOdd)
mov $3,%bx
mul %bx /* %dx:%ax = %ax*3 */
mov $2,%bx
div %bx /* %dx:%ax/2 */
cmp $0,%bx /* remainder %dx = 0 ? */
jz LABEL_EVEN
movb $1,(bOdd)

LABEL_EVEN:
xor %dx,%dx /* Now %ax is the offset of FATEntry in FAT */
mov (BPB_BytsPerSec),%bx
div %bx /* %dx:%ax/BPB_BytsPerSec */
push %dx
mov $0,%bx
add $SecNoOfFAT1,%ax /* %ax <- FATEntry's sector */
mov $2,%cl /* Read 2 sectors in 1 time, because FATEntry */
call ReadSector /* may be in 2 sectors. */
pop %dx
add %dx,%bx
mov %es:(%bx),%ax
cmpb $1,(bOdd)
jnz LABEL_EVEN_2
shr $4,%ax

LABEL_EVEN_2:
and $0x0fff,%ax

LABEL_GET_FAT_ENTRY_OK:
pop %bx
pop %es
ret

.org 510 /* Skip to address 0x510. */
.2byte 0xaa55 /* Write boot flag to 1st sector(512 bytes) end */
::::::::::::::
loader.S
::::::::::::::
.code16
.text
mov $0xb800,%ax
movw %ax,%gs
mov $0xf,%ah
mov $'L',%al
mov %ax,%gs:((80*0+39)*2)
jmp .
::::::::::::::
solrex_x86_boot.ld
::::::::::::::
SECTIONS
{
. = 0x7c00;
.text :
{
_ftext = .;
} = 0
}
::::::::::::::
solrex_x86_dos.ld
::::::::::::::
SECTIONS
{
. = 0x0100;
.text :
{
_ftext = .;
} = 0
}
::::::::::::::
Makefile
::::::::::::::
CC=gcc
LD=ld
OBJCOPY=objcopy

CFLAGS=-c
TRIM_FLAGS=-R .pdr -R .comment -R.note -S -O binary

LDFILE_BOOT=solrex_x86_boot.ld
LDFILE_DOS=solrex_x86_dos.ld
LDFLAGS_BOOT=-e c -T$(LDFILE_BOOT)
LDFLAGS_DOS=-e c -T$(LDFILE_DOS)

all: boot.img LOADER.BIN

boot.bin: boot.S
$(CC) $(CFLAGS) boot.S
$(LD) boot.o -o boot.elf $(LDFLAGS_BOOT)
$(OBJCOPY) $(TRIM_FLAGS) boot.elf $@

LOADER.BIN: loader.S
$(CC) $(CFLAGS) loader.S
$(LD) loader.o -o loader.elf $(LDFLAGS_DOS)
$(OBJCOPY) $(TRIM_FLAGS) loader.elf $@

boot.img: boot.bin
dd if=/dev/zero of=emptydisk.img bs=512 count=2880
dd if=boot.bin of=boot.img bs=512 count=1
dd if=emptydisk.img of=boot.img skip=1 seek=1 bs=512 count=2879
rm emptydisk.img

# You must have the authority to do mount, or you must use "su root" or
# "sudo" command to do "make copy"
copy: boot.img LOADER.BIN
mkdir -p /tmp/floppy;
mount -o loop boot.img /tmp/floppy/ -o fat=12;
cp LOADER.BIN /tmp/floppy/;
umount /tmp/floppy/;
rm -rf /tmp/floppy/;

clean:
@rm -f *.o *.elf *.bin *.BIN

distclean:
@rm -f *.img

在 Linux 上制作数据软盘或者 CD 镜像

当在 Linux 下和虚拟机共享文件时,我发现一个问题,并不是所有的虚拟机都支持(或很好的支持)在主机和虚拟机操作系统之间共享文件,这样在需要文件共享时候就会遇到很多问题。不过因为几乎所有操作系统都支持读写软盘和光盘,就可以使用一种比较迂回的办法解决这个问题:建立软盘或者光盘镜像文件,虚拟机使用读写软/光驱的方式打开它,Linux 主机直接挂载它,这样就可以将这个文件作为共享目录使用。不过当虚拟机的操作系统足够强大的时候,完全可以使用虚拟网络传输。

这样就涉及到如何建立一个数据软/光盘镜像。我们平常所挂载的光盘镜像,基本是用于只读的目的,而且是别人建立好的。如何使用 Linux 命令来建立一个数据软/光盘镜像呢?

建立光盘镜像很简单,使用下面命令即可:

genisoimage -o data.iso /data/yourdir/*

这就能直接将 /data/yourdir/ 下的所有文件建立成一个光盘镜像。但是使用光盘镜像唯一一个不足就是,它是只读的,所以文件的共享只能从一个方向进行(如果虚拟机上的系统不支持烧录CD的话 ^_^)。

建立软盘镜像:

前面在 Writing x86 PC Bootloader With Free Software 一文中提到了如何制作启动软盘镜像,但是这个软盘镜像仅仅能用来启动电脑,而不能用来存储数据。因为它没有被格式化,所以不能挂载,因此最主要的任务是要将它格式化。

首先,生成空白软盘镜像:
dd if=/dev/zero of=data.img bs=512 count=2880

使用 losetup 命令,将 data.img 作为 loop device 使用:
sudo losetup /dev/loop0 data.img

然后,格式化这个 loop device:
sudo mkfs.msdos /dev/loop0

检查文件系统:
sudo fsck.msdos /dev/loop0

删除 loop device:
sudo losetup -d /dev/loop0

这时候,data.img 已经格式化完成,可以作为一个软盘镜像使用,比如用 sudo mount -o loop data.img mountdir/ 挂载到 mountdir 上。设置一定的权限之后,使用虚拟机打开这个软盘镜像,就可以把这个镜像当作共享目录来使用,虽然很可怜的是这个文件只有 1.44 M 大小,但在某些时候已经能满足需要了。

Fedora 和 Ubuntu 上的段错误

LilyBBS discussion: Segmentation Fault on Fedora and Ubuntu

昨天和别人在小百合 LinuxUnix 版发帖讨论 Segmentation Fault 的问题,整理如下:

flyDutchMan 根据自己存在段错误的程序在 Fedora 和 Ubuntu 上的运行结果,认为 Fedora 和 Ubuntu 对段错误的处理方式不同,他的观点是(原文链接:[href: http://bbs.nju.edu.cn/vd83468/bbscon?board=LinuxUnix&file=M.1185514732.A&num=6528 ] ):

“Ubuntu认为段错误是很严重的错误,它的做法是当即中断程序。而Fedora对待段错误是比较宽容的,在Fedora中即使检测到某个进程正在对不属于它的地址空间进行操作,他仍然会完成这次“非法”的操作,并且继续执行后面的操作,只是在终端上打印出“Segment Fault”的错误。所以在这个程序中,虽然发生了段错误,Fedora仍然能运行到connect(),是整个程序顺利跑起来。”

并给出了一个 demo:

#include <stdio.h>

#define IP_ADDR_LENGTH 16
#define UMP_FUNC_NUM 6

int main() {
  char addr[IP_ADDR_LENGTH];
  addr[0] = '';
  strcat(addr, "172.16.64.181" );

  char (*taskpath)[IP_ADDR_LENGTH];

  printf("the address is %dn", &amp;taskpath[0]);

  int a;
  int b;

  sprintf(taskpath[0], "%s", addr);

  printf("taskpath[0]: %sn", taskpath[0]);
  printf("finish!n");
  fflush(stdout);

  return 0;
}

“在Ubuntu下运行,系统不会输出“ finish! ”这句,而是在输出taskpath[0]的地址后直接终止程序。注意,上面的int a; int b;声明不能省略,也不能赋值!因为如果省略或赋值,就不会产生Segment Fault!(赋值的话系统就会把这两个变量分配到Stack中,这就与对Heap操作的taskpath没有冲突了)”

我的回复是(原文链接:[href: http://bbs.nju.edu.cn/vd83468/bbscon?board=LinuxUnix&file=M.1185532985.A&num=6538 ]):

首先,我的观点,没有所谓 Fedora 和 Ubuntu 对段错误处理的不同。因为它们都是使用 Linux kernel,而内存管理只要 Kernel 的版本一样,我认为不会有不同的处理方式。

其次,我想纠正上文中的一个说法(可能有些讨人嫌哈,不过一些东西还是说清楚点儿好,因为 ls 用这个来解释自己的程序):

> 赋值的话系统就会把这两个变量分配到Stack中,这就与对Heap操作的taskpath没有冲突了

无论你赋值与否,这两个变量都是存在在 stack 中的; taskpath 也不是对 heap 进行操作,它只是存在于 stack 上的一个指针变量。

> 因为如果省略或赋值,就不会产生Segment Fault!

在我的系统中,都会产生段错误。

最后,我来给出我对这个问题的解释:

就上文的 demo 程序来说, a, b, addr, taskpath 都是存在于 stack 上的,这个很清楚,调试过 C 语言程序的人应该都知道,我就不解释了。

1. 为什么会出现段错误?

因为 taskpath 是一个野指针,在使用之前没有被赋值,所以 taskpath 会指向任何位置,对一个随机的位置进行写操作,显然会出现段错误。

2. 为什么同一个程序,定义不定义 a, b 会影响段错误出来的时间点?

虽然上面说 taskpath 会指向任何位置,但是这个说法并不完全正确。因为大家知道,taskpath 是在 stack 上的一个变量,而 stack 呢,是一直在重复使用的一个区域。要明确这一个概念,在操作系统中执行一个可执行文件,程序并不是从 main 开始的,它要先执行一段代码,也就是平常所说的 crt(c runtime)。这个一般是由 lib 提供的,其中要调用一些库函数,所以呢,在 main 执行之前, stack 被 crt 用过(这是最关键的一点)。

因为 stack 使用完是会被释放的,这也就是在调用函数时 function prologue 和 epilogue 所干的事情,开辟栈空间和恢复栈空间,主要动作就是移动栈指针。那么 taskpath 所占的位置很有可能被 crt 用过(不是一定),那么如果被 crt 写过,比如被 crt 用做保留 ebp 或者什么其他的寄存器,它的值就是确定的(在一定程度上说)。

如果 crt 在 taskpath 这个位置上保存过寄存器的值,尤其是 ebp 或者 esp,那么很有可能 taskpath 就指向此程序栈空间的某个位置。那么写 taskpath 指向的内存产生的段错误就没那么 critical,或者说操作系统对它的指针在自己栈空间中的操作比较容忍,就不会立即停止程序的运行。但是如果 crt 没有在这个位置上进行操作,那么这个位置就可能是某个垃圾地址,比如说操作系统自己的内存空间,那么写 taskpath 指向的内存就会造成很严重的后果,操作系统会立马检查出来终止它的运行。

我在 Ubuntu 7.04 下使用 gcc 4.1.2 编译、调试并反汇编的结果显示:两个程序唯一的不同是 taskpath 在堆栈上的位置,当定义 int a, b; 时,taskpath 是 $ebp -40 而这个地址没有被操作过;当不定义 int a, b; 时, taskpath 是 $ebp - 32,这个地址曾经被 crt 使用过。所以按照上面的解释,系统报段错误的时间不一样。

如果熟悉 GDB 的话,可以很容易的用调试证明这一点。计算出 crt 入口的 $ebp 和 main 中 $ebp 的差,以此计算出 taskpath 保存的位置,在上面设置 watch point,从 _init 执行到 main,看其中有没有对 taskpath 所在位置进行写操作。

3. 为什么不同的操作系统,结果不一样?
这个就比较简单了。kernel 不一样,可能内存管理的方式不一样。使用的 lib 或者 gcc 不一样,可能引起 crt 的汇编结果不一样。这两个都能导致同样的程序报错的时间不一样。

所以,不是 Fedora 或者 Ubuntu 能不能容忍段错误,没有 OS 容忍段错误,不同只是在产生段错误够不够 crucial 需要得到立即处理。

Hacking 《自己动手写操作系统》Chapter 1&2

——Writing x86 PC Bootloader With Free Software

本文内容已整理成一本电子书,请到这里下载

今天在看一本书,《自己动手写操作系统》(于渊,电子工业出版社),虽然很欢迎这样一本详尽介绍怎样写操作系统的书出现,但看完前两章后对作者的某些做法很不以为然。比如使用 Windows 作为开发平台,采用商业虚拟机作为测试平台,不是每个人都买得起这些软件的(我们要在心中牢固树立使用盗版软件就是犯罪的观念 X-D)。

我这篇文章的目的就是为了展示 Linux/Free Software 的强大,不使用任何商业软件,不用自己写的任何工具,使用免费的工具链,照样可以完成而且更高效地完成《自己动手写操作系统》前两章的 demo。

当然我也希望这篇文章为推动 Linux 和 Free Software 的发展出一点力,比如让部分读者(尤其是 EE 和 CS 学生)看完这篇文章后舍弃瘟都死,投身到 Linux 和 Free Software 阵营中来,或者《自己动手写操作系统》的作者在下一版(如果有的话)中完全使用 Free Software 来做 demo。

好了,下面开始,先介绍一下需要使用的工具。
操作系统:Ubuntu 7.04 Feisty,平台: i386 PC。
使用工具:gcc, binutils(as, ld, objcopy), dd, make, hexdump, vim, virtualbox。
上面所说的工具中,除了 virtualbox 虚拟机,剩下的工具在任何能用做开发环境的 Linux 版本上都是默认安装的。VirtualBox 也是遵从 GPL 协议的开源软件,可以从这里 [href: http://www.virtualbox.org ]下载。Ubuntu 只需要 sudo apt-get install virtualbox 安装即可。

首先,我们看第一个示例代码:
[wbyang@solrex-PC loader]$ more boot.asm
org 07c00h ; 告诉编译器程序加载到7c00处
mov ax, cs
mov ds, ax
mov es, ax
call DispStr ; 调用显示字符串例程
jmp $ ; 无限循环
DispStr:
mov ax, BootMessage
mov bp, ax ; ES:BP = 串地址
mov cx, 16 ; CX = 串长度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 000ch ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮)
mov dl, 0
int 10h ; 10h 号中断
ret
BootMessage: db "Hello, OS world!"
times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 0xaa55 ; 结束标志
此段代码使用 Intel 格式的汇编语言写成,本也是用同样开源的 NASM 编译,但是鉴于很少有人在 Linux 下使用此汇编语法,它在 Linux 平台上的扩展性和可调试性都不好(GCC 不兼容),而且不是采用 Linux 平台上编译习惯,所以我把它改成了使用 GNU tool chain 去编译连接。这样的话,对以后使用 GNU Toolchain 编写其它体系结构的 bootloader 也有帮助,毕竟 NASM 没有 GAS 用户多。

上面的汇编文件可以用 AT&T 风格改写为:
[wbyang@solrex-PC loader]$ more boot.S
.code16 ;使用16位模式汇编(GAS 默认认为 .S 文件是 pure 32-bits i386 code)
.text ;代码段开始(为 link script 做定位)
mov %cs,%ax
mov %ax,%ds
mov %ax,%es
call DispStr ;调用显示字符串例程
INF: jmp INF ;无限循环(GAS 没有 $ 作为当前行标号的约定)
DispStr:
mov $BootMessage, %ax
mov %ax,%bp ; ES:BP = 串地址
mov $16,%cx ; CX = 串长度
mov $0x1301,%ax ; AH = 13, AL = 01h
mov $0x00c,%bx ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮)
mov $0,%dl
int $0x10 ; 10h 号中断
ret
BootMessage:.ascii "Hello, OS world!"
.org 510 ; 填充到 510 字节,使生成的二进制代码恰好为512字节
.word 0xaa55 ; 结束标志

但有一个问题, NASM 可以直接使用 nasm boot.asm -o boot.bin 将 boot.asm 编译成 bin 文件,GAS 不能。但是 GAS 的不能恰好给开发者一个机会去分步地实现这个过程,使编译更为灵活。下面请看 link script 和 Makefile:
[wbyang@solrex-PC loader]$ more solrex_x86.ld
SECTIONS
{
. = 0x7c00;
.text :
{
_ftext = .; //告诉链接器程序加载到内存的7c00处
} = 0
}

这个连接脚本的功能就是,在 link 的时候,将程序加载到内存 0x7c00 的位置(BOIS 将 PC 控制权转交给这个位置运行的程序),相当于 boot.asm 中的 org 07c00h 一句。有人可能觉得麻烦,还需要用一个脚本控制加载地址,但是 《自己动手写操作系统》就给了一个很好的反例:Chapter 1.5 代码 1-2,作者切换调试和运行模式时候需要对代码进行注释,而使用脚本控制,只需要编译时候调用不同脚本进行连接,就能解决这个问题。

这在嵌入式编程中是非常常见的处理方式,使用不同的连接脚本一次 make 生成某个程序分别运行在 board 上和 simulator 上的两个二进制文件 。

相信只要能耐心看到这里的人对下面 Makefile 中的内容都不会陌生。
[wbyang@solrex-PC loader]$ more Makefile
CC=gcc
LD=ld
LDFILE=solrex_x86.ld #使用上面提供的连接脚本 solrex_x86.ld
OBJCOPY=objcopy

all: boot.img

boot.img: boot.bin
@dd if=/dev/zero of=emptydisk.img bs=512 count=2880 #生成空白软盘镜像文件
@dd if=boot.bin of=boot.img bs=512 count=1 #用 bin file 生成对应的镜像文件
@dd if=emptydisk.img of=boot.img skip=1 seek=1 bs=512 count=2879 #在 bin 生成的镜像文件后补上空白,最后成为合适大小的软盘镜像

boot.bin: boot.elf
@$(OBJCOPY) -R .pdr -R .comment -R.note -S -O binary boot.elf boot.bin

boot.elf: boot.o
$(LD) boot.o -o boot.elf -e c -Tsolrex_x86.ld

boot.o: boot.S
$(CC) -c boot.S

clean:
@rm -rf boot.o boot.elf boot.bin boot.img

使用 Makefile,一个是方便,直接就可以生成可引导的软盘镜像文件,二是为了清楚,其间对源文件的任何处理都一清二楚。下面解释一下 Makefile 都做了什么:
第一步, gcc 调用 as 将 boot.S 编译成目标文件 boot.o;
第二步, ld 调用连接脚本 solrex_x86.ld 将 boot.o 连接成可执行文件 boot.elf。
第三步, objcopy 移除 boot.elf 中没有用的 section(.pdf,.comment,.note),strip 掉所有符号信息,输出为二进制文件 boot.bin。
第四步, dd 使用 boot.bin 生成可引导的软盘镜像 boot.img。

到此,我们使用开源工具链编译生成 bootloader 的过程已经结束,没有使用到任何商业软件,也没有自己写任何转换工具。《自己动手写操作系统》文中提到的 HD-COPY 和 Floopy Writer 都没有使用到。 为了验证也可以先用 hexdump -x -n 512 boot.img 将 boot.img 前 512 个字节打印出来,可以看到 boot.img dump 的内容和附送光盘中的 TINIX.IMG dump 的内容完全相同。这里我们也显然用不到 EditPlus 或者 UltraEdit,即使需要修改二进制码,也可以使用 vim 的 %!xxd 命令进行十六进制编辑。

写得真够累的,那个 virtualbox 就不说了,用法跟 virtualPC 没有什么大差别。安装上以后,新建一个虚拟机,加载 boot.img 光盘镜像到软驱,然后选择虚拟机重新启动,就可以看到红色的:
Hello, OS world!
当然,你如果喜欢的话,可以改成 "F-U_C-K Microsoft!" 不过要注意的一点是,做这件事是需要权限的,要把当前用户加到 vboxusers 组中,否则这个行为无法成功 :-)。

综上,很显然 VirtualPC 也不需要。《自己动手写操作系统》可怜的作者费了半天劲在瘟都死的 VirtualPC 里装 Redhat Linux,解决瘟都死和 Linux 的文件共享问题,解释了一番自己“工欲善其事,必先利其器”的动机,但我实在没看出有一点儿需要瘟都死的原因。瘟都死什么都不能给你,但 Linux 能给你整个世界!

类成员函数作为 pthread_create 函数参数

花了三个工作日把原来写的一段通信守护进程代码从过程方法改到了 template class,对于 template 的使用和类的派生明白了不少道理。还有个很受启发的一点,就是 C++ 中如何使用类的成员函数作为创建线程的开始函数。
pthread_create 是 POSIX 标准下创建线程的函数,函数原型是:
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);

在 C 中,这个函数使用很简单,只要定义一个参数和返回值均为 void * 类型的函数,使用函数名字作为参数即可。就算不完全符合,可以使用 (void *(*)(void *)) 将其强制转换为符合类型检查规格的函数指针。但是,类的非静态成员函数隐含 this 指针作为第一个参数,所以参数完全不可能转化为 void * 类型,而 C++ 的类型检查要比 C 严格许多。由于我原来写的代码是 C 风格的,自然不会出现类型不符的问题,现在将线程开始函数封装到一个模板类中,再创建线程的时候就不能满足需要了。

在试了几种转换无效之后,从网上搜到一种方法:定义线程开始函数为类的静态成员函数(static member function),这样就不隐含 this 指针了,然后将 this 指针从 pthread_create 最后一个参数传给开始函数,在函数中将 void * 类型的 this 指针强制转换为类指针。看来静态成员函数还是有些妙用的。

===================我叫分割线===================

带着我妈和我妹在北京城里转悠了几天,累得不行。主要原因在我,没有考虑到身体因素,连着玩消耗太大,再加上自己也没车没房,倒公交车和住宾馆也要走很远,费时间又费劲。我都快受不了了,别说我妈了。以后再出去旅游,坚决不会再连着转三天以上,要么紧紧张张地玩两天,要么就花时间长点儿,走走歇歇。唉,谁让咱是穷人呢,又有钱又有闲的日子还没过上呢!

今天看了下积压很久的博客订阅,同学里开始写和继续写的人越来越多了。觉得有些文章比较阴沉低迷,因为自己从大三开始心情就老是跌宕起伏,反而在面对这许多次的分别聚首时坦然一些。要不是周熹在散伙饭时候专门招我,也不会哭得那么厉害。散了散了散了吧,没有离别,怎么会有重逢呢?

还是在 Yourui 的博客上看到小恪去新疆的消息,要是当面看到他,肯定会玩笑说发配三千里伊犁充军去了。这在边关待了两年之后,再回来学积分拓扑之类的数学还能看得下去吗?都说是命运无常,旅途坎坷却能看更美风景,只是不知道那关外还是不是大漠孤烟长河落日的大西北?

刚才去吃饭,走在晚间暖暖的懒洋洋的空气中,忽然有点儿秋天的感觉。想起 7 年前爸爸送我到商丘一高上学的情景,日子可真快啊!老了,老了!

又回到北京了

好些天没有更新博客了(针对我的更新频率而言),原因很简单,有更重要的事情去做 :-)。

1 号到的北京,把寄放在同学那里的被子拖到租住的公寓,请邓飞、丽君吃了顿饭。2 号到公司报到,换了个办公桌,换了个电话。寄来的包裹和录取通知书同时到达,纳闷不已,为什么北京往南京挂号需要七天而南京挂号到北京只需要三天?害得我为了一张纸折腾(中科院的录取通知书着实简陋,信封是牛皮小信封,通知书就是一张请柬大小)。我原以为是通知书的那玩意儿是入学须知,但搞不明白的是,为什么入学须知要寄给我两次?

一毕业,这同学们好久不更新的博客都重新拾起来了,反而显得我有点儿懒。这下到公司工作,晚上的时间就多了些,可能又要恢复我正常的更新速度了。

这两天工作也没干什么事,FPGA 调试还不太熟练,模拟器又改了不少,而且更新中工作不太正常,所以就随便看看代码、看看邮件列表和一些 patch。GDB 的 mailing list 里这两天挺有意思的,有个人发了一个希望把 GDB 代码用 C++ 重写的邮件,然后一群大佬说这个问题别讨论了,各持己见讨论起来会没个头的。但是某个邮件里有巨牛的一句话:“The more C++ code I see, the more convinced I get that the language should die. ”让我叹服不已,哈哈。

重装了一个 Ubuntu,当然也装上了 beryl,比原来用着更方便了。Google desktop for linux 推出了,试用了一下,挺好的,特别喜欢它的两下 ctrl 就可以弹出搜索框,这样就免得我还得用鼠标把光标定位在 Google toolbar 里去。Google desktop 的 sidebar 没有在 Linux 版本上体现出来比较遗憾,那些小工具很有意思,但其实 Linux 本身就带有很多有一些的小玩意儿,喜欢玩的能把桌面配置得很 cool 或者 cute。为 Firefox 装了一个 IE7 的主题,看起来还是蛮好的,看来 Microsoft 的 UI 工程师也不是吃干饭的。但是 wine 现在还不支持 IE7,用 ies4linux 测试版装上之后,IE7 的 UI 效果一点儿也没有,真没劲。

毕业了,大家都在干不同的事,还有人(XHO)感叹没假期了,哈哈,准备成家立业的时候已经开始了,慢慢就该习惯了。

从中科大音乐站抓MP3的一个脚本

由于南京大学的 COS 音乐 FTP 被关闭了,原来很喜欢 COS 按歌手和专辑分门别类的风格,现在就不知道到哪里去下歌了。今天发现了一个好的音乐网站:中国科学技术大学音乐站,[href: http://music2.ustc.edu.cn ],按歌手和专辑分类,在教育网内速度算是相当快的,可以在线听。我就动了心思,想把专辑自动下下来。

中科大音乐站是这样的,你先选择歌手、专辑和歌曲,点击"播放"会下载下来一个 music@ustc.m3u 文件,使用播放软件打开就可以在线播放选中的歌曲。其 music@ustc.m3u 文件中每首歌的条目如下例:

#EXTM3U 双节棍
http://music2.ustc.edu.cn:8088/6e6bbc6495aec1f5fc719dcbc826ab4c%2F64%2Ff05523485d63a9c422393215c676f194.mp3

可以看出,中科大音乐站在存储 mp3 音乐文件的时候采取了某种文件名加密方法,很难直接猜出文件名,而且在提交 form 的时候采取 post 方式,不知道它的 action 文件接受什么样的参数,所以想在这一步上减少工作量是很难的。而且从网站上拿下来这个列表并不算很麻烦,因为每个人基本只会对某些专辑感兴趣嘛。那么下面要做的工作就是自动处理这个列表,将歌曲下载下来存在应该的位置和保存为正确的文件名。

Linux Bash Shell 脚本如下:

for album in $(ls *.m3u)
do
# Test the encoding of music list file.
  encode=$(file -i $album | awk ’{print $3}’ | sed -n ’s/charset=//p’)
if [ $encode != "utf-8" ]
then
  iconv -f gbk -t utf8 $album &gt; ${album}.1
  mv ${album}.1 $album
fi
# Test if the album already exists.
if [ ! -d ${album%.*} ]
then
  mkdir -p ${album%.*}
fi
# Get the url list.
urls=(`grep -v "#" $album`)
# Download the song.
N=0
for song in $(grep "#" $album | sed -n ’s/#EXTM3U //p’ | sed ’s/ /_/g’)
do
  echo "Processing $song..."
  if [ ! -f ${album%.*}/${song}.mp3 ]
  then
    wget -O ${album%.*}/${song}.mp3 ${urls[$N]}
  fi
N=`expr $N + 1`
done
done

这个脚本所做的工作是:
1.找到当前目录下所有的以 m3u 为后缀名的专辑播放列表文件,循环处理它们。
2.检查专辑列表文件的编码方式,如果不是 utf-8 格式编码,将文件编码转换成 utf-8(为了处理歌曲文件名需要)。
3.去掉专辑列表文件后缀名 .m3u,以此为名字建立专辑目录。
4.得到专辑中歌曲的 url 列表。
5.下载歌曲到专辑目录中,并以对应的文件名命名,歌曲文件名中的空格被转化为下划线。

使用方法,先手动下载希望下载的每个专辑的播放列表(.m3u 文件),并将其文件名改为对应的专辑名,放到与脚本同一目录下,运行脚本,就会将每个专辑中的歌曲下载到以专辑名命名的目录下。