Patch for Libjingle with GCC 4.2.4 on Ubuntu

It is a svn diff result, not a patch, actually.

So, what is Libjingle? Quoted from http://code.google.com/p/libjingle/:

Libjingle, the Google Talk Voice and P2P Interoperability Library, is a set of components we provide to interoperate with Google Talk's peer-to-peer file sharing and voice calling capabilities. The package includes source code for Google's implementation of Jingle and Jingle-Audio, two proposed extensions to the XMPP standard that are currently available in draft form.

You can check out the head revision of Libjingle from its svn repository using command:

svn checkout http://libjingle.googlecode.com/svn/trunk/ libjingle-read-only

Then ``./autogen.sh'' and ``make'' as we usually do for building a *nix software. You will find many errors during ``./autogen.sh'' and ``make''. To fix them, first, some LIBs should be installed:

sudo apt-get install build-essential libexpat1-dev libglib2.0-dev libogg-dev libssl-dev libasound2-dev libspeex-dev openssl libortp7-dev libmediastreamer0-dev libavcodec-dev

I am not very sure if these LIBs are enough. If you have some problem with this, please let me know.

Even if you have all of these LIBs installed, you will still get some errors such as:

../../talk/base/stringutils.h:272: error: extra qualification 'talk_base::Traits::' on member 'empty_str'
../../talk/base/base64.h:26: error: extra qualification ‘talk_base::Base64::’ on member ‘Base64Table’
../../talk/base/base64.h:27: error: extra qualification ‘talk_base::Base64::’ on member ‘DecodeTable’

So here is a patch for source code errors like this. IMPORTANT NOTE: gcc version 4.2.4 on Ubuntu 8.04, libortp7.

Index: talk/p2p/base/sessionmanager.h
===================================================================
--- talk/p2p/base/sessionmanager.h    (revision 7)
+++ talk/p2p/base/sessionmanager.h    (working copy)
@@ -156,7 +156,7 @@

   // Creates and returns an error message from the given components.  The
   // caller is responsible for deleting this.
-  buzz::XmlElement* SessionManager::CreateErrorMessage(
+  buzz::XmlElement* CreateErrorMessage(
       const buzz::XmlElement* stanza,
       const buzz::QName& name,
       const std::string& type,
Index: talk/session/phone/linphonemediaengine.cc
===================================================================
--- talk/session/phone/linphonemediaengine.cc    (revision 7)
+++ talk/session/phone/linphonemediaengine.cc    (working copy)
@@ -80,24 +80,24 @@
     }
#endif
#ifdef HAVE_SPEEX
-    if (i->name == speex_wb.mime_type && i->clockrate == speex_wb.clock_rate) {
-      rtp_profile_set_payload(&av_profile, i->id, &speex_wb);
-    } else if (i->name == speex_nb.mime_type && i->clockrate == speex_nb.clock_rate) {
-      rtp_profile_set_payload(&av_profile, i->id, &speex_nb);
+    if (i->name == payload_type_speex_wb.mime_type && i->clockrate == payload_type_speex_wb.clock_rate) {
+      rtp_profile_set_payload(&av_profile, i->id, &payload_type_speex_wb);
+    } else if (i->name == payload_type_speex_nb.mime_type && i->clockrate == payload_type_speex_nb.clock_rate) {
+      rtp_profile_set_payload(&av_profile, i->id, &payload_type_speex_nb);
     }
#endif

     if (i->id == 0)
-      rtp_profile_set_payload(&av_profile, 0, &pcmu8000);
+      rtp_profile_set_payload(&av_profile, 0, &payload_type_pcmu8000);

-    if (i->name == telephone_event.mime_type) {
-      rtp_profile_set_payload(&av_profile, i->id, &telephone_event);
+    if (i->name == payload_type_telephone_event.mime_type) {
+      rtp_profile_set_payload(&av_profile, i->id, &payload_type_telephone_event);
     }
    
     if (first) {
       LOG(LS_INFO) << "Using " << i->name << "/" << i->clockrate;
       pt_ = i->id;
-      audio_stream_ = audio_stream_start(&av_profile, 2000, "127.0.0.1", 3000, i->id, 250);
+      audio_stream_ = audio_stream_start(&av_profile, 2000, (char *)"127.0.0.1", 3000, i->id, 250);
       first = false;
     }
   }
@@ -106,7 +106,7 @@
     // We're being asked to set an empty list of codecs. This will only happen when
     // working with a buggy client; let's try PCMU.
      LOG(LS_WARNING) << "Received empty list of codces; using PCMU/8000";
-    audio_stream_ = audio_stream_start(&av_profile, 2000, "127.0.0.1", 3000, 0, 250);
+    audio_stream_ = audio_stream_start(&av_profile, 2000, (char *)"127.0.0.1", 3000, 0, 250);
   }
 
}
@@ -114,12 +114,12 @@
bool LinphoneMediaEngine::FindCodec(const Codec &c) {
   if (c.id == 0)
     return true;
-  if (c.name == telephone_event.mime_type)
+  if (c.name == payload_type_telephone_event.mime_type)
     return true;
#ifdef HAVE_SPEEX
-  if (c.name == speex_wb.mime_type && c.clockrate == speex_wb.clock_rate)
+  if (c.name == payload_type_speex_wb.mime_type && c.clockrate == payload_type_speex_wb.clock_rate)
     return true;
-  if (c.name == speex_nb.mime_type && c.clockrate == speex_nb.clock_rate)
+  if (c.name == payload_type_speex_nb.mime_type && c.clockrate == payload_type_speex_nb.clock_rate)
     return true;
#endif
#ifdef HAVE_ILBC
@@ -171,8 +171,8 @@
#ifdef HAVE_SPEEX
   ms_speex_codec_init();

-  codecs_.push_back(Codec(110, speex_wb.mime_type, speex_wb.clock_rate, 0, 1, 8));
-  codecs_.push_back(Codec(111, speex_nb.mime_type, speex_nb.clock_rate, 0, 1, 7));
+  codecs_.push_back(Codec(110, payload_type_speex_wb.mime_type, payload_type_speex_wb.clock_rate, 0, 1, 8));
+  codecs_.push_back(Codec(111, payload_type_speex_nb.mime_type, payload_type_speex_nb.clock_rate, 0, 1, 7));
  
#endif

@@ -181,8 +181,8 @@
   codecs_.push_back(Codec(102, payload_type_ilbc.mime_type, payload_type_ilbc.clock_rate, 0, 1, 4));
#endif

-  codecs_.push_back(Codec(0, pcmu8000.mime_type, pcmu8000.clock_rate, 0, 1, 2));
-  codecs_.push_back(Codec(101, telephone_event.mime_type, telephone_event.clock_rate, 0, 1, 1));
+  codecs_.push_back(Codec(0, payload_type_pcmu8000.mime_type, payload_type_pcmu8000.clock_rate, 0, 1, 2));
+  codecs_.push_back(Codec(101, payload_type_telephone_event.mime_type, payload_type_telephone_event.clock_rate, 0, 1, 1));
   return true;
}

Index: talk/xmpp/xmppclient.h
===================================================================
--- talk/xmpp/xmppclient.h    (revision 7)
+++ talk/xmpp/xmppclient.h    (working copy)
@@ -138,7 +138,7 @@
     }
   }

-  std::string XmppClient::GetStateName(int state) const {
+  std::string GetStateName(int state) const {
     switch (state) {
       case STATE_PRE_XMPP_LOGIN:      return "PRE_XMPP_LOGIN";
       case STATE_START_XMPP_LOGIN:  return "START_XMPP_LOGIN";
Index: talk/third_party/mediastreamer/msrtprecv.c
===================================================================
--- talk/third_party/mediastreamer/msrtprecv.c    (revision 7)
+++ talk/third_party/mediastreamer/msrtprecv.c    (working copy)
@@ -26,7 +26,7 @@
MSMessage *msgb_2_ms_message(mblk_t* mp){
     MSMessage *msg;
     MSBuffer *msbuf;
-    if (mp->b_datap->ref_count!=1) return NULL; /* cannot handle properly non-unique buffers*/
+    if (mp->b_datap->db_ref!=1) return NULL; /* cannot handle properly non-unique buffers*/
     /* create a MSBuffer using the mblk_t buffer */
     msg=ms_message_alloc();
     msbuf=ms_buffer_alloc(0);
@@ -120,7 +120,7 @@
         gint got=0;
         /* we are connected with queues (surely for video)*/
         /* use the sync system time to compute a timestamp */
-        PayloadType *pt=rtp_profile_get_payload(r->rtpsession->profile,r->rtpsession->payload_type);
+        PayloadType *pt=rtp_profile_get_payload(r->rtpsession->rcv.profile,r->rtpsession->rcv.telephone_events_pt);
         if (pt==NULL) {
             ms_warning("ms_rtp_recv_process(): NULL RtpPayload- skipping.");
             return;
Index: talk/third_party/mediastreamer/audiostream.c
===================================================================
--- talk/third_party/mediastreamer/audiostream.c    (revision 7)
+++ talk/third_party/mediastreamer/audiostream.c    (working copy)
@@ -112,7 +112,7 @@
             RtpSession **recvsend){
     RtpSession *rtpr;
     rtpr=rtp_session_new(RTP_SESSION_SENDRECV);
-    rtp_session_max_buf_size_set(rtpr,MAX_RTP_SIZE);
+    rtp_session_set_recv_buf_size(rtpr,MAX_RTP_SIZE);
     rtp_session_set_profile(rtpr,profile);
     rtp_session_set_local_addr(rtpr,get_local_addr_for(remip),locport);
     if (remport>0) rtp_session_set_remote_addr(rtpr,remip,remport);
@@ -133,7 +133,7 @@
     /* creates two rtp filters to recv send streams (remote part)*/
    
     rtps=rtp_session_new(RTP_SESSION_SENDONLY);
-    rtp_session_max_buf_size_set(rtps,MAX_RTP_SIZE);
+    rtp_session_set_recv_buf_size(rtps,MAX_RTP_SIZE);
     rtp_session_set_profile(rtps,profile);
#ifdef INET6
     rtp_session_set_local_addr(rtps,"::",locport+2);
@@ -147,7 +147,7 @@
     rtp_session_set_jitter_compensation(rtps,jitt_comp);
    
     rtpr=rtp_session_new(RTP_SESSION_RECVONLY);
-    rtp_session_max_buf_size_set(rtpr,MAX_RTP_SIZE);
+    rtp_session_set_recv_buf_size(rtpr,MAX_RTP_SIZE);
     rtp_session_set_profile(rtpr,profile);
#ifdef INET6
     rtp_session_set_local_addr(rtpr,"::",locport);
@@ -217,8 +217,8 @@
     ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_FREQ,&pt->clock_rate);
     ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_BITRATE,&pt->normal_bitrate);
    
-    ms_filter_set_property(stream->encoder,MS_FILTER_PROPERTY_FMTP, (void*)pt->fmtp);
-    ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_FMTP,(void*)pt->fmtp);
+    ms_filter_set_property(stream->encoder,MS_FILTER_PROPERTY_FMTP, (void*)pt->send_fmtp);
+    ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_FMTP,(void*)pt->send_fmtp);
     /* create the synchronisation source */
     stream->timer=ms_timer_new();
    
Index: talk/third_party/mediastreamer/msrtpsend.c
===================================================================
--- talk/third_party/mediastreamer/msrtpsend.c    (revision 7)
+++ talk/third_party/mediastreamer/msrtpsend.c    (working copy)
@@ -85,7 +85,7 @@
{
     guint32 clockts;
     /* use the sync system time to compute a timestamp */
-    PayloadType *pt=rtp_profile_get_payload(r->rtpsession->profile,r->rtpsession->payload_type);
+    PayloadType *pt=rtp_profile_get_payload(r->rtpsession->snd.profile,r->rtpsession->snd.telephone_events_pt);
     g_return_val_if_fail(pt!=NULL,0);
     clockts=(guint32)(((double)synctime * (double)pt->clock_rate)/1000.0);
     ms_trace("ms_rtp_send_process: sync->time=%i clock=%i",synctime,clockts);
Index: talk/base/base64.h
===================================================================
--- talk/base/base64.h    (revision 7)
+++ talk/base/base64.h    (working copy)
@@ -23,8 +23,8 @@
   static std::string decode(const std::string & data);
   static std::string encodeFromArray(const char * data, size_t len);
private:
-  static const std::string Base64::Base64Table;
-  static const std::string::size_type Base64::DecodeTable[];
+  static const std::string Base64Table;
+  static const std::string::size_type DecodeTable[];
};

} // namespace talk_base
Index: talk/base/stringutils.h
===================================================================
--- talk/base/stringutils.h    (revision 7)
+++ talk/base/stringutils.h    (working copy)
@@ -269,7 +269,7 @@
template<>
struct Traits<char> {
   typedef std::string string;
-  inline static const char* Traits<char>::empty_str() { return ""; }
+  inline static const char* empty_str() { return ""; }
};

///////////////////////////////////////////////////////////////////////////////

You killed all these errors? Congratulations! You can start talking with your gtalk friends with command ``call'' in talk/examples/call/ !

PS: If you are working with GCC 4.3.x, more strict checking is applied on the code. However, most errors can be fixed by adding some C headers into the #include fields, such as: <cstdlib>, <cstring>.

应用程序打包技术之四(exe篇)

1. 应用程序打包技术之一(源代码篇)
2. 应用程序打包技术之二(deb篇)
3. 应用程序打包技术之三(rpm 篇)
4. 应用程序打包技术之四(exe篇)

exe 是 Windows 下通用应用程序的后缀,因此也是 Windows 下安装程序的后缀,我就以 exe 篇来命名这篇文章吧。

我们在 Windows 下写程序,尤其是带有图形界面的应用程序,往往不仅需要一个可执行的 exe 文件,还需要一些辅助的 dll 或者资源文件。一般情况下,我们可以采用打包整个目录的方式来发布软件(也是目前很多所谓“绿色软件”的做法),当然我们也可以发布传统的 Windows 安装文件。这样用户不需要额外的解压缩软件,双击执行安装文件就可以安装软件。

HM NIS Edit

将 Win 下应用程序打包成安装文件的方法有很多,这里我们主要介绍一个开源的安装文件制作工具 NSIS(Nullsoft Scriptable Install System)。比如我们耳熟能详的软件 WiNamp 和 eMule 的安装文件,就是使用 NSIS 生成的。它的主页在http://nsis.sourceforge.net,您可以从它的主页下载到最新版本。

NSIS 是一个基于脚本的安装文件制作工具,因此我们必须写好一个脚本提供给它执行。总是要学一些“微语言”真是麻烦的事情,幸好我们还有 HM NIS Edit,一个开源的 NSIS 脚本编辑工具。使用 HM NIS Edit,我们可以一步一步地按照向导生成 NSIS 所需的脚本。因此,我们这篇文章其实主要是介绍 HM NIS Edit 的使用,和 NSIS 的部分脚本格式。使用 NSIS 脚本生成安装文件那一步,实在是太简单不过了,点一下鼠标或者敲一个命令而已。

假设我们要为 foo 程序生成一个安装文件,在打包之前您应该将 foo 的可执行程序、所需 dll 和资源文件等放在一个文件夹 foo 中。比如假设 foo 程序的目录是这样的。

D:>tree /F foo
D:FOO
│  foo.dll
│  foo.exe
│  licence.txt
│
└─image
        foo.ico

下面我们就可以使用 HM NIS Edit 来创建 NSIS 脚本了。启动 HM NIS Edit,在 File 菜单中选择 New script from wizard 脚本生成向导,点击 Next 下一步,进入程序信息页。

Application name 程序名这里我们填入要打包的程序名 "foo";Application version 程序版本我们填入 foo 的版本,比如 "1.0";Application publisher 发布者我们填程序的开发公司名或者自己的名字,比如 "张三";Application website 程序主页我们填程序的主页名,没有主页的就可以不填。然后点击下一步进入安装文件选项页。

Setup icon 安装图标是您希望您的安装文件长什么样子,而不是您应用程序的图标,一般选择默认即可;Setup file 是您希望安装文件叫什么名字,比如 "foo-1.0-setup.exe";Setup lang 安装程序语言是安装过程中的提示所使用的语言,您可以根据您的需要选择,比如简体中文 "SimpChinese";GUI 是安装文件的对话框风格,随便您喜欢哪种;Compress 压缩格式是您希望使用什么格式将应用程序压缩存放在安装文件中。然后点击下一步进入应用程序默认安装目录和协议页。

Application default directory 里面填您的应用程序默认安装到哪里,比如 $PROGRAMFILESfoo 是默认安装到 C:Program Filesfoo 目录下,最好勾选上 Allow user to change the application directory,允许用户更改安装目录,这样您的程序显得更人性化一点儿;License file 是指应用程序所使用的协议文本,如果您在 foo 目录下已经准备了协议文件 license.txt,那么直接填 licence.txt 即可。这个协议就是您通常在 Windows 下安装软件时,第一个页面提示的“是否同意上述协议”的“上述协议”文本框里的内容;下面几个选项是让用户选择如何接受协议。点击下一步,进入应用程序文件选择页。

在应用程序文件选择页中会有三个文本框。这个页面的作用是分组添加应用程序所需要的程序文件,这样用户安装时就可以通过选择“最小安装”、“完全安装”、“自定义”等选择安装不同的组件。左上方的文本框是组件框,右下方的文本框是组件信息说明框,右方最大的文本框是每个组件所包含的可执行、dll 和资源文件。如果我们的程序很简单,不用分什么组件,我们就只用一个 MainSection 就行了。点中左上方文本框中的 MainSection,在右侧将所有程序文件添加进去。由于我们已经将所有文件都放置在了 D:foo 目录下,我们只需要点选第二个图标:Add directory tree,在对话框中将源目录选择为 F:Moviefoo,目标目录选择为 $INSTDIR,这样 foo 下所有的文件和目录都将会被安装到 $INSTDIR(默认是 C:Program Filesfoo)目录下。确定之后返回文件选择页,点击下一步进入应用程序图标页。

应用程序图标页的主要作用是选择将会被安装到“桌面”和“开始”菜单的快捷方式指向的可执行程序。如果您的程序名和项目名一样,或者 foo 目录下只有一个 exe 可执行文件,此处就使用默认设置即可。Create an Internet shortcut in the Start Menu folder 的意思是在“开始”菜单中添加一个到软件主页的快捷方式;Create an Uninstall shortcut in the Start Menu folder 的意思是在“开始”菜单中添加一个到卸载程序快捷方式。点击下一步进入安装后执行设置页。

安装后执行的意思是当安装程序安装完成后,用户选择安装后直接启动应用程序或者查看自述文件时,程序的行为。如果您有自述文件,就在 Readme 中填入自述文件的名字,比如 readme.txt,如果没有,就什么也不填,直接进入下一步程序卸载选项。

如果您选择了使用卸载程序 Use uninstaller,NSIS 将会为您自动生成一个卸载程序,其选项使用默认即可。点击下一步进入结束页。

最后结束时,HM NIS Edit 会询问您是否保存脚本。当然要保存了,保存了以后再需要生成安装文件时就不必使用 HM NIS Edit 重新生成脚本了。Convert file paths to relative paths 将脚本中的文件路径修改成相对于脚本文件的路径,这个选项也最好选上,这样在更改 foo 的目录时,我们只需要 NSIS 脚本与 foo 的相对位置不变就不影响脚本的使用。接下来保存脚本文件,最好将脚本文件保存在 foo 目录下,这样以后需要重新生成安装文件,只需要将 NSIS 拷贝到 foo 目录下就可以编译了。比如取名为 foo.nsi。

这样,整个脚本文件我们已经编写好了。现在我们到 D:foo 目录下,就能发现一个 foo.nsi 文件,右键点击 foo.nsi,在下拉菜单中选择 Compile NSIS script,不出错的话,就能在当前目录下生成一个名为 foo-1.0-setup.exe 安装文件了。您可以双击执行一下它,看看安装过程是否如您所料。

我们也可以使用命令行编译 NSIS 脚本,您可以使用这个命令:

C:Program FilesNSISmakensis.exe foo.nsi

如果您将 C:Program FilesNSIS 添加到了 PATH 环境变量中,就可以直接使用 makensis.exe foo.nsi 来编译了。

小技巧:

1. 当生成 NSIS 脚本之后,我们想修改设置,不需要重新执行一遍脚本生成向导。只需要用文本编辑器打开 foo.nsi,找到相应的域,更改设置即可。

2. NSIS 是一个相当强大的安装文件生成器,但是使用 HM NIS Edit 脚本生成向导生成的脚本并不具有很灵活的定制性。如果您需要更多特性,请阅读 NSIS 用户手册,您能从网上搜索到该手册的中文版本。然后直接去修改 NSIS 脚本。

3. 用 NSIS 产生的卸载程序有可能会产生卸载不干净的现象,主要原因是 NSIS 卸载程序不支持递归删除目录。如果您想要它把所有文件和目录都删除的话,就需要在 Section Uninstall 中将所有程序可能会生成的文件和目录都添加进去,这样生成的卸载程序就能卸载全部文件和目录了。

4. 您可以在这里找到更漂亮的图文教程。

应用程序打包技术之三(rpm 篇)

1. 应用程序打包技术之一(源代码篇)
2. 应用程序打包技术之二(deb篇)
3. 应用程序打包技术之三(rpm 篇)
4. 应用程序打包技术之四(exe篇)

rpm 是 RedHat 系 Linux 使用的软件包格式。流行的 Linux 发行版:Fedora, RHEL, OpenSUSE, Oracle 包括国产的红旗 Linux,都采用 rpm 来管理软件包。

我不是很喜欢 rpm 软件包格式,原因主要有两个,一个是它的依赖关系很难处理,另一个是控制文件比较复杂。但是 rpm 包有着非常广泛的应用,也是一个提高生产力的重要工具。

像前一篇文章提到的一样,checkinstall 也可以用来打 rpm 包,但我不是很熟悉这个软件。这里我仅仅介绍如何使用原始的 rpm(rpmbuild) 工具来打 rpm 软件包。

和前面 deb 包使用 fakeroot 一样,rpmbuild 也是利用沙盒的方式来构建软件包,除了控制文件比较繁琐,过程可能还更自动一些。构建 rpm 包的工作目录默认是 /usr/src/redhat,如果您在 Debian 下安装了 rpm 这个软件包,它可能是 /usr/src/rpm。您可以使用 rpm --eval %_topdir 查看自己的 rpm 工作目录。

$ tree `rpm --eval %_topdir`
/usr/src/redhat
|-- BUILD
|-- RPMS
|   |-- athlon
|   |-- i386
|   |-- i486
|   |-- i586
|   |-- i686
|   `-- noarch
|-- SOURCES
|-- SPECS
`-- SRPMS

我们可以看到,/usr/src/redhat/ 下有几个子目录:SOURCES 用来存放源代码包,SPECS 用来存放 spec 控制文件,BUILD 用来解压源代码包和构建软件,RPMS 里存放的是打好的二进制应用程序 RPM 包,SRPMS 里存放的是打好的源代码 RPM 包。但是,我们应该知道 _topdir 是可以在 spec 文件中修改的,否则我们只能用 root 才能在默认的工作目录 /usr/src/redhat 下创建文件和执行命令。我们可以先把工作目录树拷贝到用户自己的目录中:

$ cp -r /usr/src/redhat ~/rpm

这样,我们就可以在用户目录 ~/rpm 下打 rpm 包了。打 rpm 包之前的准备工作只有两件:1. 准备好源代码包并放置在 SOURCES 目录下;2. 准备好 spec 控制文件并放在 SPECS 目录下。如何打源代码包我们已经在 《源代码篇》中介绍过,下面我们主要来介绍 spec 文件的格式。下面是一个实例 spec 文件。

$ more ~/rpm/SPECS/casnet.spec
%define   _topdir    /home/solrex/rpm
Name:     casnet
Version:  1.3
Release:  1
License:  GPL
Packager: Solrex Yang
Summary:  CASNET Client
Group:    Network
Source:   %{name}-%{version}.tar.gz
URL:      http://share.solrex.org/casnet/
Prefix:   /usr

%description
CASNET is a gui client for ip gateway of GUCAS(Graduate University of Chinese
Academy of Sciences), which is written in Python and PyGtk.

%prep
%setup -q

%build

%install
make -e PREFIX=%{prefix} install

%files
%{prefix}/bin/casnetconf
%{prefix}/bin/casnet
%{prefix}/bin/casnet-gui
%{prefix}/share/casnet/casnetconf.py
%{prefix}/share/casnet/casnet.py
%{prefix}/share/casnet/casnet-gui.py
%{prefix}/share/casnet/pics/*.png
%{prefix}/share/applications/casnet.desktop
%{prefix}/share/icons/casnet.png

我们可以看到,spec 文件与 deb 包的 control 文件有很多相似之处:Name, Version, Release, License, Packager, URL 是软件的名称、版本、小版本、使用的协议、维护者和网址;Summary 和 %description 是软件的信息;Group 是软件所归类别。剩下的就有些不同了:Source 是指软件的源代码包名称,rpm 会到 SOURCES 目录下找这个包;Prefix 是软件的默认安装位置;%prep、%build、%install 下分别是软件的预处理、编译和安装命令;%files 是所有需要被打包进去的文件。

因此,作者需要将 spec 文件中的各个域填写正确。%prep 下的 %setup 宏一般是用来将源代码 tar 包释放到 BUILD 目录下;%build 下可以添加 %configure 宏和 make 命令,如果您的软件需要编译的话;%install 下是您的安装命令;%files 下是您程序运行所需要的全部文件,其路径即为您安装完软件后它应该在的位置。%{name} 可以用来指代 Name: 域的内容,%{version } 指代 Version: 域的内容,以此类推。

需要注意的是,rpmbuild 使用 BUILD/%{name}-%{version} 作为您的源代码包释放后的目录,进入其进行编译。因此当您使用 tar 打源代码包时,tar 包的顶层目录名应该为 %{name}-%{version} 而不是仅仅是 %{name},比如:

$ tar czvf casnet-1.3.tar.gz casnet-1.3
$ mv casnet-1.3.tar.gz ~/rpm/SOURCES

现在我们在 ~/rpm/SOURCES 下有了 casnet-1.3.tar.gz,在 ~/rpm/SPECS 目录下有了 casnet.spec,那么我们就可以使用下面的命令生成 rpm 包了。如果您的安装位置 Prefix 选择的是一个系统目录,比如 /usr, /usr/local 之类,您也许需要使用 root 权限来运行 rpmbuild 命令。

$ rpmbuild -ba ~/rpm/SPECS/casnet.spec

这样我们就能在 ~/rpm/RPMS/i386 目录下获得我们生成的 rpm 软件包了。rpmbuild 命令的执行过程是这样的:首先根据 spec 文件定位源代码包,然后将源代码包释放到 BUILD 目录下,使用 spec 文件中给出的命令编译和安装,然后将 spec 中列的文件提取出来,按照包信息打出来 rpm 包。

我们可以使用 rpm -qpi 命令来查看 rpm 包的信息,rpm -qpl 命令来查看 rpm 包所包含的文件列表:

$ rpm -qpi ~/rpm/RPMS/i386/casnet-1.3-1.i386.rpm
Name        : casnet                       Relocations: /usr
Version     : 1.3                               Vendor: (none)
Release     : 1                             Build Date: Thu 26 Feb 2009 08:03:50 PM CST
Install Date: (not installed)               Build Host: laptop
Group       : Network                       Source RPM: casnet-1.3-1.src.rpm
Size        : 40883                            License: GPL
Signature   : (none)
Packager    : Solrex Yang
URL         : http://share.solrex.org/casnet/
Summary     : CASNET Client
Description :
CASNET is a gui client for ip gateway of GUCAS(Graduate University of Chinese
Academy of Sciences), which is written in Python and PyGtk.

$ rpm -qpl ~/rpm/RPMS/i386/casnet-1.3-1.i386.rpm
/usr/bin/casnet
/usr/bin/casnet-gui
/usr/bin/casnetconf
/usr/share/applications/casnet.desktop
/usr/share/casnet/casnet-gui.py
/usr/share/casnet/casnet.py
/usr/share/casnet/casnetconf.py
/usr/share/casnet/pics/casnet.png
/usr/share/casnet/pics/offline.png
/usr/share/casnet/pics/online.png
/usr/share/icons/casnet.png

如果您想了解更详细的内容,您可以进一步参考 Jake's RPM Build Tutorial 这篇文章。

应用程序打包技术之二(deb篇)

1. 应用程序打包技术之一(源代码篇)
2. 应用程序打包技术之二(deb篇)
3. 应用程序打包技术之三(rpm 篇)
4. 应用程序打包技术之四(exe篇)

deb 是 Debian 系 Linux 使用的软件包格式,也是我最欣赏的软件包格式。我所知道的打 deb 软件包的方法有两种,一种是使用 checkinstall,另一种是使用 dpkg。

checkinstall 不仅仅可以用来打 deb 包,还可以打 rpm 和 tgz 包,而且使用方法相对简单。但是 checkinstall 的运行不是那么稳定,我搞不懂它在什么情况下才能正常运行,而且它的定制性不是很强,使用时老是要交互地输入些信息,所以我还是放弃了使用它来打包软件。感兴趣的朋友可以在网上搜索一下这个程序的使用方法。

dpkg 是 Debian 的“原生”包管理软件,但是很多人不太愿意使用 dpkg 来打包 deb。究其原因可能是需要写麻烦的配置文件,但是写配置文件的一个好处就是在下次打包时候可以直接用上次的配置文件,只修改一个版本号就可以了,而不用每次都需要填包信息。在介绍如何打 deb 包之前,我们现看一下如何解 deb 包。

$ sudo apt-get install tree
$ dpkg -X /var/cache/apt/archives/tree_1.5.1.1-1_i386.deb fakeroot
$ cd fakeroot
$ dpkg -e /var/cache/apt/archives/tree_1.5.1.1-1_i386.deb
$ tree
.
|-- DEBIAN
|   |-- control
|   `-- md5sums
`-- usr
    |-- bin
    |   `-- tree
    `-- share
        |-- doc
        |   `-- tree
        |       |-- README
        |       |-- changelog.Debian.gz
        |       |-- changelog.gz
        |       `-- copyright
        `-- man
            `-- man1
                `-- tree.1.gz

dpkg -X 是将 deb 包的内容文件释放出来,dpkg -e 是将 deb 包的控制信息释放出来。前面执行那个 sudo apt-get install tree 是为了将 tree_1.5.1.1-1_i386.deb 下载到本地 apt cache,如果您已经安装过 tree 这个软件,可以为 apt-get 加上 -d 参数,使其只下载而不安装。

从上面 tree 命令的执行结果我们发现,deb 包解开后分两部分:一部分是控制信息,在 DEBIAN 目录下;一部分是安装内容,在 usr 目录下。现在您大概明白为什么我们使用 fakeroot 作为目录名了,因为这个目录就是一个"假根目录",您在这个目录下所有的修改,最后都会被映射到目标机的根目录 / 下。比如 fakeroot/usr/bin/tree 这个文件,就会被安装到 /usr/bin 下,以此类推。

只要您能理解 fakeroot 这个目录映射,您就知道如何安放自己的文件了。为了让生成的包将文件 foo 安装到目录 /usr/xx/yy 目录下,您只用在 fakeroot 目录下建立 usr/xx/yy 目录,并将 foo 拷贝进去就行了。

好,下面进入关键的配置文件部分,关于 control 和 md5sums。

$ more DEBIAN/control
Package: tree
Version: 1.5.1.1-1
Architecture: i386
Maintainer: Ubuntu MOTU Developers
Original-Maintainer: Florian Ernst
Installed-Size: 92
Depends: libc6 (>= 2.6-1)
Section: utils
Priority: optional
Description: displays directory tree, in color
Displays an indented directory tree, using the same color assignments as
ls, via the LS_COLORS environment variable.
.
Homepage: http://mama.indstate.edu/users/ice/tree/

我们可以看到,control 文件中包含的主要是软件的版本和维护者信息,我相信大家都能基本看懂上面这些信息什么意思:Package 包名(tree)、Version 版本(1.5.1.1-1)、Architecture 目标机架构(i386 386及以后)、Maintainer 维护者(Ubuntu MOTU Developers)、Original-Maintainer 原维护者(Florian Ernst)、Installed-Size 安装后大小(92K)、Depends 依赖软件包(libc6 不低于 2.6-1 版本)、Section 包分类(工具)、Priority 优先级(可选)、Description 包描述、Homepage 软件主页。

由于咱们分析这个包是 Ubuntu 发布的包,所以包信息给的比较全,其实并不是上面所有的信息都有必要提供(小声说一句,就算全提供也不是很难吧?除了咱不用的,Original-Maintainer 这种就算了)。关于哪些信息比较重要,以及每个域的具体含义和可选项,可以参考 Debian 的文档 Debian Policy Manual Chapter 5 - Control files and their fields

您也可以依样画葫芦,写一个类似的 control 文件放到 DEBIAN 目录下,提供一些自己软件包的信息,基本有这个配置文件就可以打包了。

$ more DEBIAN/md5sums
d60a3b4736f761dd1108cb89e58b9d4e usr/bin/tree
981ea0343c2a3eb37d5fc8b5ac5562df usr/share/man/man1/tree.1.gz
483a56158a07a730ec60fc36b3f81282 usr/share/doc/tree/README
ea56d78ae0d54693ae8f3c0908deeeff usr/share/doc/tree/copyright
4456e04c3c268eabcd10ee9b949a9b9a usr/share/doc/tree/changelog.gz
ec104db6914cfce2865a0d8c421512bb usr/share/doc/tree/changelog.Debian.gz

md5sums,这文件名一看,就知道是保存着软件包中各文件的 md5 校验值,用来校验软件包是否被损坏了。其实这个文件纯属“腊月三十逮兔子,有它没它都过年”,您可以完全不提供它。

这样呢,我们就准备好了 deb 包的内容文件和控制信息:控制文件放在了 fakeroot/DEBIAN 目录下,内容文件放在 fakeroot/usr 下,目录树就像开头 tree 命令的结果。下面只需要一个命令就能打出来 deb 安装包了:

$ cd ..
$ dpkg -b fakeroot/ foo.deb

这时候当前目录下就出现了 foo.deb。您可以使用 dpkg -I foo.deb 查看 foo.deb 的控制信息,dpkg -c foo.deb 查看 foo.deb 包含了什么文件,sudo dpkg -i 安装 foo.deb。

小技巧:

1. 如果您懒得自己新建一个控制文件和目录树,您完全可以像本文开头那样,找一个简单的软件包,将它的内容和控制信息释放出来,对它进行修改,然后打出来自己的包。

2. 生成 md5sums 文件不是什么难事,您只需要在 fakeroot 目录使用下面这个命令:

$ md5sum `find usr -type f` > DEBIAN/md5sums
或者
$ find usr/ -type f -exec md5sum {} + > DEBIAN/md5sums

3. 将您的可执行文件拷贝到 fakeroot/usr 下并不一定要手动一个个拷。如果您使用 GNU 自动工具集,./configure 时加个参数 --prefix=fakeroot/usr/ 即可;如果您自己写的 Makefile,可以在 Makefile 中使用一个变量 PREFIX=/usr,当您不加参数时,make install 的安装目标就是 /usr 下,您可以使用 Makefile -e PREFIX=fakeroot/usr/ install 来覆盖 Makefile 中的变量设置。

应用程序打包技术之一(源代码篇)

1. 应用程序打包技术之一(源代码篇)
2. 应用程序打包技术之二(deb篇)
3. 应用程序打包技术之三(rpm 篇)
4. 应用程序打包技术之四(exe篇)

相信很多朋友都曾经为方便做某件事写过自己的小程序(像我写过的 casnetsendsms),但很多怕都是藏在深山没人识,最后不了了之,自己也把它们丢在角落里忘记了。

把这些小工具上传到技术论坛或者 CSDN 下载频道之类的网站,还是能收到一些关注的,而且还能攒积分和声望。但是为什么不把它们发布出去呢?估计有几个原因:源代码太乱,编译又挺复杂,不好意思给别人看;二进制程序包不会打,不知道该怎么发布。

因此我打算在本系列文章中分享一下我的程序打包经验,目前来讲计划有四篇:源代码篇、deb 篇、rpm 篇和 exe 篇。这些技术在网上您都能找到,因为我也是从网上学习的,算是一个笔记和汇总吧。如果您有好的批评或建议,不妨在评论中指出,帮助我完善这篇文章。

源代码篇

发行源代码包是件最简单的事情,因此也在最先介绍。有同学会说,打个压缩包不就完了嘛。的确如此,但是在压缩之前您也要有几个注意事项:

1. 删除版本管理目录,比如 .svn,.cvs 之类的目录。像 Subversion 版本管理软件,会在每个目录下都建立名为 .svn 的目录,里面一般保存着目录下文件的最新版本,可以用来 revert 修改。不删除 .svn 目录,会使源代码包臃肿,而且最重要的是可能会有安全隐患。.svn 目录下还包含您的用户名和 SVN 服务器信息,可能您并不想让别人知道。但是如果您网速够快的话,可以重新 svn export 一份,而不是仅仅从您的 svn 树上拷贝一份出来,那就没有 .svn 目录了。

2. 规整一下编译过程,如果编译过程不规范,您应该添加一个 README。如果您的代码不是脚本,很可能是需要用户编译的。如果编译过程规范,*nix(Linux/Unix, CygWin, Mingw 等) 下就是 ./configure, make, make install,用户很容易理解。但如果编译过程不规范,您就最好添加一下 README 或者 INSTALL 文件,指导用户该如何编译。使用 MS VisualStudio 的用户要注意工程文件的整洁性,最好导出一个 Makefile(是的,VS 也可以用 Makefile),这样用户就不必打开项目,在 CMD 命令行用 nmake Makefile 就可以编译了。

3. 删除二进制中间文件。在 *nix 开发者中,这一般不难做到,Makefile 中一般都会写一个 clean 目标;但是 MS VS 用户一般不会注意那些编译时生成的 .obj 文件。源代码包就应该是源代码,最多包含可执行程序和文档,而不应该包含其它任何二进制的文件。否则您的源代码包就会很大,而且对用户也是困扰,到底哪些文件有用呢?

4. 修改编译目标从 debug 版本到 release 版本。*nix 下,这一般意味着 CFLAGS 要改成 -O2 而不是 -g;VS 一般意味着将目标从 debug 转为 release。这样用户生成的可执行程序才能足够小和足够快,他们如果有能力自己调试,会知道如何将选项改回去的。

5. 添加知识产权信息,就是授权协议。小程序大家一般都不在乎,但如果是您在这个项目上花了足够的心血,还是最好选择一个自己喜欢的授权协议。可以将协议声明放在每个源文件的最前注释中,也可以在项目的根目录下放一个 license 文件。

6. 不要用 rar 包。在 Windows 下推荐使用 zip 格式压缩;*nix 下推荐使用 .tar.gz 或者 .tar.bz2 格式。因为这些格式在这些系统上不需要安装额外的软件解压。WinRAR 是一个商业软件,而且 RAR 格式也是受版权保护的。

打包命令:

在 Windows 下,如果您使用开源软件 7-zip 来压缩 zip 包,可以使用这个命令(首先将 7-zip 可执行文件目录添加到 PATH 环境变量中):

7z a foo.zip foo

*nix 下,就是下面几个命令了:

tar czvf foo.tar.gz foo
tar cjvf foo.tar.bz2 foo
zip -r foo.zip foo

尽量别使用 Py2exe for Python 2.6

Py2exe 是用来将 Python 程序打包成 Windows 下可执行 exe 程序的工具。这样那些未安装 Python 开发环境的用户就可以直接使用 Python 写的软件了。

Py2exe 并不是把 Python 程序编译成 Windows 的原生程序,而是将运行 Python 所需的 dll, lib 等打包到一起供 Python 程序使用。一个很短的 Python 程序往往会生成几兆的软件包。因此 Py2exe 打包程序的执行效率并不会有提升,只是方便初级 Windows 用户使用罢了。

除了 Py2exe 之外,还有一些其它的 Python 到 exe 的打包程序,比如 Pyinstaller、cx_Freeze 等。它们在某些情况下表现比 Py2exe 要好,但是在兼容性和用户群支持上不如 Py2exe(一家之见)。

前两天我图新鲜,把 Windows 中的 Python 升级到了 2.6,PyGtk 和 Py2exe 也随之升级到了支持 2.6 的版本。然后问题就来了,用 Py2exe for Python 2.6 打包的 PyGtk 程序在打包的机器上运行正常,但拷贝到部分人的 Windows 中后却无法运行,点击就出现

“由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。”

刚开始我以为是 Win 下 GTK 库 dll 的问题,反复地验证几次觉得应该问题不在 GTK 上。如果缺少外部库,Python 应该报缺少 dll 问题,而不是应用程序出错。怀疑是缺少微软的库,用 PE Explorer 试用版在出问题的电脑上扫描一下应用程序的依赖关系,发现缺少 msvcr90.dll,将 msvcr90.dll 拷贝到程序包中,再扫描依赖关系,所有的 dll 已经都满足了,仍然报出同样的错误。

后来基本确定是 Python 2.6 的问题。因为 Python 2.6 是使用 Microsoft Visual C++ 2008 编译的,所以要想 py2exe for 2.6 打包的程序运行,目标机器上必须装有 Python 2.6 或者 Microsoft Visual C++ 2008 Redistributable Package。否则系统就无法识别 exe 程序的 CRT, 因而它就成为无法运行的程序。

之所以程序在一部分人的机器上运行正常,是因为这些人 Windows 中安装了 VC2008 开发套件,自然也就包括了 VC2008 运行时库。

因为我们发布程序时无法强制每个人都去安装 Microsoft Visual C++ 2008 Redistributable Package,所以需要发布 exe 程序时,还是使用老版本的 Python 2.5 和 Py2exe for Python 2.5,别使用 Python 2.6 为好。

Python 不支持杀死子线程

昨天为我的 casnet 程序添加新功能。其中一个功能是断线自动重连,本来是单线程的程序,添加这个功能就需要后台有一个线程定时地查询当前状态,如果掉线就自动重连。因之遇到了一个如何设计这个守护线程的问题。

我刚开始的想法是后台线程每次运行查询后 sleep 一段时间,然后再运行查询。但是我马上遇到了一个问题:当主程序退出时,后台线程仍在运行,主窗口无法退出。

在使用其它的库时,比如 POSIX 的 pthread,可以使用 ptread_cancel(tid) 在主线程中结束子线程。但是 Python 的线程库不支持这样做,理由是我们不应该强制地结束一个线程,这样会带来很多隐患,应该让该线程自己结束自己。所以在 Python 中,推荐的一种方法是在子线程中循环判断一个标志位,在主线程中改变该标志位,子线程读到标志位改变,就结束自己。

import threading

class X(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    self.flag = 1

  def run(self):
    while self.flag == 1:
      sleep(300)
      ...

如果直接使用这种方法,那么我前面的设计就会出现问题。因为线程会被 sleep 阻塞一段时间,那么只有在 sleep 的间隙,才有可能去读取标志位。这样主线程需要等待当前 sleep 结束才能使子线程退出,进而整个程序才能退出。这种做法是行不通的,你不可能指望用户点击“关闭窗口”后等待几百秒程序才能退出。

当然,也可以使用系统命令 kill 来杀死整个进程。但问题是这样做既不 graceful,又不能保证代码对不同系统的兼容性。

只好换个思路,从原来后台进程的设计改起。定时执行未必非得使用 sleep,也可以像 crontab 那样判断当前时间能不能整除某个值,但这样做不能保证任务在某个时间间隔内只执行一次,因为除数的精度和任务的执行时间不好把握;或者使用 timer,但是 timer 会带来更多线程,增加了复杂度。

于是最后决定使用解决 Feedbuner 图标定时抓取问题的方法。在线程中保存上次查询时间,比较当前时间与上次查询时间的差,若大于某个值,就进行查询并更新保存的时间。

  def run(self):
    self.last = time.time()
    while self.flag == 1:
      Now = time.time()
      if Now - self.last > 300:
         self.last = Now
         ...

这样就既能保证子线程在 flag 改变之后尽快退出,又能保证在指定时间间隔内任务只运行一次。但是网友 earthengine 兄指出这种方法并不妥,代码中不用 sleep 就变成了忙循环,这样会造成 CPU 使用率过高的问题,仅仅在循环中间添加一个 sleep(0~1) 就能大幅度地降低 CPU 使用,而且关闭程序时 1 秒钟以内的延迟对于用户来说一般还是可以接受的。

  def run(self):
    self.last = time.time()
    while self.flag == 1:
      sleep(1)
      Now = time.time()
      if Now - self.last > 300:
         self.last = Now
         ...

再深入思考一下,虽然本文中的后台线程从功能上来看似乎用不着考虑太多同步的问题,但最后的退出过程可视为一个线程同步的过程。因此可以采用线程同步的思想来设计后台线程:在正常工作时,后台线程进行带超时的等待,超时后就执行工作;退出时主线程给后台线程发送一个信号,由于后台线程在超时等待,因此接收信号后就终止退出。这样,在用户结束程序时,就不用等待 sleep 到时了。

import threading

class X(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    self.flag = 1
    self.cond = threading.Condition()

  def run(self):
    self.cond.acquire()
    self.condition.wait(300)
    while self.flag == 1:
      ...
      self.cond.release()
      self.cond.acquire()
      self.condition.wait(300)

...
x.flag = 0
x.cond.acquire()
x.cond.notify()
x.cond.release()

最后,非常感谢 earthengine 兄的精彩评论,小弟受益良多。

Feedburner 订阅数图标显示解决办法

很多人以前都用过 Feedburner 烧制自己的 rss feed,但是由于众所周知的原因,Feedburner 的 rss 输出在中国网,封天下无法访问了(不信您可以点击一下我博客右侧的 Feedburner 图标)。虽然用 Google Reader 订阅 Feedburner 的 feed 仍然不受影响,但是博客订阅数图标无法正常显示,所以很多人好奇我是如何让 Feedburner 订阅数图标仍然显示在博客侧栏的

更新:刚才写完,我想看看 feed 有没有更新,忽然发现 Feedburner 的 rss 可以访问了,试了试订阅数图标,也能正常显示了。GFW 打盹了?我说这几天 Feedburner 订阅数增加那么快呢。总之我这篇文章算是白写了......呜呜呜呜,没有提前重现问题的后果。

又更新:12 月 23 日,我又无法访问 feedburner 的 RSS 了,才一天那!看来我这篇文章还算没有完全白写,不能幻想 GFW 的仁慈。

其实我以前是用的这篇博客里的方法。这个方法要求你有个国外主机空间,碰巧我能使用师兄的空间,把那篇文章中的 feedburner.php 上传到空间里就可以使用了。

但是使用过程中我发现这个方法有个很严重的问题:不支持并发访问。这是由于它的方法太生硬,先读取自己文件的内容,如果文件中写的时间比当前时间早 4 个小时,就下载新的订阅数图标,重写自身文件(修改更新时间那一行),并将订阅数图标附在文件最后。注意到这里,它会重写自身文件,一个 php 文件读取自己,改一行再重新写入自己,那么如果多个用户同时访问该文件,那不就乱套了?

所以我对它进行了修改,改为一个相对干净的方法:抓取订阅数统计图标保存为一个 gif 文件,每次访问 php 文件时,php 去判断当前时间与该 gif 文件最后修改时间的差,如果大于过期时间,就重新抓取订阅数统计图标更新 gif 文件,最后将访问重定向到 gif 文件。点击这里 http://solrex.org/feedburner.php 查看效果。

具体 php 代码如下(其实我本想用 file_get_contents 函数的,但发现不好用,只好还用这个原来的 httpSocketConnection 函数了,显得冗长了些):

<?php
$username = "username"// Feedburner account name.
$expire_time = 3600;   // Expire time(in second, 3600s = 1 hour).

$fb_url = "feeds.feedburner.com";
$gif_path = "/~fc/".$username."?bg=99CCFF&fg=444444&anim=0";
$localfile = "fb_".$username.".gif";

if(!function_exists('httpSocketConnection')){
  function httpSocketConnection($host, $method, $path, $data)
  {
    $method = strtoupper($method);
    if ($method == "GET") {
      $path.= '?'.$data;
    }
    $filePointer = fsockopen($host, 80, $errorNumber, $errorString);
    if (!$filePointer) {
      return false;
    }
    $requestHeader = $method." ".$path."  HTTP/1.1\r\n";
    $requestHeader.= "Host: ".$host."\r\n";
    $requestHeader.= "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0\r\n";
    $requestHeader.= "Content-Type: application/x-www-form-urlencoded\r\n";
    if ($method == "POST") {
      $requestHeader.= "Content-Length: ".strlen($data)."\r\n";
    }
    $requestHeader.= "Connection: close\r\n\r\n";
    if ($method == "POST") {
      $requestHeader.= $data;
    }          
    fwrite($filePointer, $requestHeader);
    $responseHeader = '';
    $responseContent = '';
    do {
      $responseHeader.= fread($filePointer, 1);
    }
    while (!preg_match('/\\r\\n\\r\\n$/', $responseHeader));
    if (!strstr($responseHeader, "Transfer-Encoding: chunked")) {
      while (!feof($filePointer)) {
        $responseContent.= fgets($filePointer, 128);
      }
    } else {
      while ($chunk_length = hexdec(fgets($filePointer))) {
        $responseContentChunk = '';
        $read_length = 0;
        while ($read_length < $chunk_length) {
          $responseContentChunk .= fread($filePointer, $chunk_length - $read_length);
          $read_length = strlen($responseContentChunk);
        }
        $responseContent.= $responseContentChunk;
    &nb
sp;   fgets($filePointer);
      }
    }
    return chop($responseContent);
  }
}

function get_fbcount($host, $path, $file)
{
  $content = httpSocketConnection($host, 'GET', $path, NULL);
  $fp = fopen( $file,"w" );
  fwrite($fp, $content);
  fclose($fp);
}

if (file_exists($localfile)) {
  $last_modified = filemtime($localfile);
  if ( date('U') - $last_modified > $expire_time) {
    get_fbcount($fb_url, $gif_path, $localfile);
  }
} else {
  get_fbcount($fb_url, $gif_path, $localfile);
}
Header("Location: $localfile");
?>

将上述代码保存为一个 php 文件,比如 fb_username.php,修改用户名和过期时间,上载到国外或者港台能正常访问 Feedburner 的主机空间中,您就可以在网页中用:

<img src="http://your.host.domain/fb_username.php" style="border: 0pt none ;" alt="Feedburner">

来引用 Feedburner 订阅计数图标了。由于这个 php 脚本按照用户名保存 gif 图标,您可以在一个服务器上为多人提供引用,只需修改 php 文件的 usrname 一项,并上传为另一个 php 文件,您也可以将这两个变量改为 php 文件的参数(不建议这样做,因为会被别人利用)。

您也可以在这里下载到 feedburner.txt,要记得将其后缀改为 php 哦。

用两个非门和任意的与、或门构造三个非门

计算机科学中有很多有趣的 puzzle,他们可能出现在那些自命清高的企业的笔试题中,也可能来源于在网路上不经意的一瞥。后者比如我在无意中访问到的 Jamie Zawinski 的个人主页:http://www.jwz.org/,他即是在著名的 Teach Yourself Programming in Ten Years 一文中,Peter Norvig 提到的那位:

One of the best programmers I ever hired had only a High School degree; he's produced a lot of great software, has his own news group, and made enough in stock options to buy his own nightclub.

Jamie Zawinski 的个人主页看起来就像是 hexdump 的结果,而且以某种规律变化着,比较奇怪的是其中埋藏的一些超级链接并不发生变化。Jamie Zawinski 在网页的源代码中这样写道:

<!-- mail me if you find the secret -->
<!--   (no, you can't have a hint)  -->

我徒劳无功地搜索了一下,没有找到任何明确的解答。如果您通过我这篇文章了解到这个 puzzle,并且解答了它的话,非常希望您也 mail 给我一份解答。

说了那么多,其实本文的主题 puzzle 却是来源于前者。这个问题更详细的阐述是:

假设有一个逻辑黑盒,三个布尔型变量 x, y, z 作为输入,输出三个布尔变量 X, Y, Z,其中:
X = ~x; Y = ~y; Z = ~z;
注意,~ 符号代表一个非门。请用两个非门和任意多个与、或门实现这个黑盒。

这个问题大概是在计算机硬件设计中提出来的,所以看起来貌似很“电子”,但是其基础却是计算机科学中的布尔代数运算。下面代码的注释中已经包含详细的算法和分析,这里我就不再解释了。(当然,这个问题的答案不是我想出来的,我只是实现并分析了一下,作为我算法学习的笔记记录在此。)

/* ===========================================================================

* Problem:
*   Construct 3 NOT gates from only 2 NOT gates(and *no* XOR gates).
*
*   Assume that a logical blackbox has three Boolean inputs x, y, z and
*   three Boolean outputs X, Y, Z where the outputs are defined as:
*     X = ~x
*     Y = ~y
*     Z = ~z
*   Note that ~ stands for a NOT gate. Please realize this blackbox using
*   only two NOT gates, and as many as possible AND and OR gates.
*
* Algorithm I:
*   Internal Nodes:
*   r = (x & y) | (x & z) | (y & z);
*   R = ~r;
*   s = (R & (x | y | z)) | (x & y & z);
*   S = ~s;
*
*   Equations for Outputs:
*   X = (R & S) | (R & s & (y | z)) | (r & S & (y & z));
*   Y = (R & S) | (R & s & (x | z)) | (r & S & (x & z));
*   Z = (R & S) | (R & s & (x | y)) | (r & S & (x & y));
*
* Analysis I:
*   We create 4 internal signals first: r, R, s and S. What equations above
*   say is that signal `r' will be 1 if two or three of the inputs are 1.
*   Meanwhile, signal `s' will be 1 if only one input is 1 or if all three
*   inputs are 1. The end result is that the two-bit word formed from `r'
*   and `s' tells us how many 1's we have[1]:
*   | r s | Means  |    | x y z | r s |    | x y z | r s |
*   |++++++++++++++|    |+++++++++++++|    |+++++++++++++|
*   | 0 0 | 0 Ones |    | 0 0 0 | 0 0 |    | 1 0 0 | 0 1 |
*   | 0 1 | 1 One  |    | 0 0 1 | 0 1 |    | 1 0 1 | 1 0 |
*   | 1 0 | 2 Ones |    | 0 1 0 | 0 1 |    | 1 1 0 | 1 0 |
*   | 1 1 | 3 Ones |    | 0 1 1 | 1 0 |    | 1 1 1 | 1 1 |
*
*   Thus now that we have the signals r and s (and their inverse
*   counterparts R and S), it's easy to construct any Boolean function of
*   x, y, and z, using only AND and OR gates:
*     X = (R & S) | (R & s & (y | z)) | (r & S & (y & z))
*   Proof:
*    1> x, y, z are all 0s, (R & S) = ~(r | s) = 1, obviously X=Y=Z=1, X = ~x;
*    2> (x, y, z) has at least one 1, R & S = 0, will be ignored, hence we
*       have:
*         X = (R & s & (y | z)) | (r & S & (y & z))
*    2.1> (x, y, z) has two or three 1s, R = ~r = 0, (R & s & (y | z)) = 0,
*         will be ignored, then we get:
*           X = S & (y & z)
*    2.1.1> (x, y, z) has three 1s, S = ~s = 0, obviously X=Y=Z=0, X = ~x;
*    2.1.2> (x, y, z) has two 1s, S = ~s = 1, will be ignored, hence we have:
*             X = y & z
*    2.1.2.1> (y, z) has one 1, x = 1, X = y & z = 1 & 0 = 0, X = ~x;
*    2.1.2.2> (y, z) has two 1s, x = 0, X = y & z = 1 & 1 = 1, X = ~x;
*    2.2> (x, y, z) has one 1, r = 0, (r & S & (y & z)) = 0, will be ignored,
*         we have:
*           X = y | z
*    2.2.1> (y, z) has one 1, x = 0, X = y | z = 1 | 0 = 1, X = ~x;
*    2.2.2> (y, z) has no 1s, x = 1, X = y | z = 0 | 0 = 0, X = ~x.
*    In conclusion, X = ~x for all cases.
*   QED.
*
* Algorithm II:
*   Internal Nodes:
*   _2or3_1s = ((x & y) | (x & z) | (y & z));
*   _0or1_1s = !(_2or3_1s);
*   _1_1     = _0or1_1s & (x | y | z);
*   _1or3_1s = _1_1 | (x & y & z);
*   _0or2_1s = !(_1or3_1s);
*   _0_1s    = _0or2_1s & _0or1_1s;
*   _2_1s    = _0or2_1s & _2or3_1s;
*
*   Equations for Outputs:
*   X = _0_1s | (_1_1 & (y | z)) | (_2_1s & (y & z));
*   Y = _0_1s | (_1_1 & (x | z)) | (_2_1s & (x & z));
*   Z = _0_1s | (_1_1 & (x | y)) | (_2_1s & (x & y));
*
* Analysis II:
*   Almost the same as Analysis I.
*
* [1] http://www.edadesignline.com/howto/191600992
* ===========================================================================
*/

#include <stdio.h>

typedef unsigned int BOOL;

int main()
{
  int i, fail;
  BOOL x, y, z, X, Y, Z;
  BOOL r, R, s, S;
  BOOL _2or3_1s, _0or1_1s, _1_1,  _1or3_1s, _0or2_1s, _0_1s, _2_1s;

  /* ==================== Algorithm I ==================== */
  printf("Algorithm I:n");
  fail = 0;
  for (i=0; i<8; i++) {
    /* Init x, y, z. */
    x = i & 1;
    y = (i>>1) & 1;
    z = (i>>2) & 1;
    /* Internal nodes. */
    r = (x & y) | (x & z) | (y & z);
    //R = !r;                               /* #1 NOT gate. */
    R = ~r & 1;                             /* #1 NOT gate. */
    s = (R & (x | y | z)) | (x & y & z);
    //S = !s;                               /* #2 NOT gate. */
    S = ~s & 1;                             /* #2 NOT gate. */
    /* Output. */
    X = (R & S) | (R & s & (y | z)) | (r & S & (y & z));
    Y = (R & S) | (R & s & (x | z)) | (r & S & (x & z));
    Z = (R & S) | (R & s & (x | y)) | (r & S & (x & y));

    if ((x == X) | (y == Y) | (z == Z)){
      fail ++;
      printf("FAIL: ");
    } else {
      printf("PASS: ");
    }
    printf("xyz = %u%u%u, XYZ = %u%u%un", x, y, z, X, Y, Z);
  }
  if (fail != 0) {
    printf("%d TEST FAILED!n", fail);
  } else if (!fail) {
    printf("ALL TEST PASSED!n");
  }

  /* ==================== Algorithm II ==================== */
  printf("Algorithm II:n");
  fail = 0;
  for (i=0; i<8; i++) {
    /* Init x, y, z. */
    x = i & 1;
    y = (i>>1) & 1;
    z = (i>>2) & 1;
    /* Internal nodes. */
    _2or3_1s = ((x & y) | (x & z) | (y & z));
    //_0or1_1s = !(_2or3_1s);               /* #1 NOT gate. */
    _0or1_1s = ~(_2or3_1s) & 1;             /* #1 NOT gate. */
    _1_1     = _0or1_1s & (x | y | z);
    _1or3_1s = _1_1 | (x & y & z);
    //_0or2_1s = !(_1or3_1s);               /* #2 NOT gate. */
    _0or2_1s = ~(_1or3_1s) & 1;             /* #2 NOT gate. */
    _0_1s    = _0or2_1s & _0or1_1s;
    _2_1s    = _0or2_1s & _2or3_1s;
    /* Output. */
    X = _0_1s | (_1_1 & (y | z)) | (_2_1s & (y & z));
    Y = _0_1s | (_1_1 & (x | z)) | (_2_1s & (x & z));
    Z = _0_1s | (_1_1 & (x | y)) | (_2_1s & (x & y));

    if ((x == X) | (y == Y) | (z == Z)){
      fail ++;
      printf("FAIL: ");
    } else {
      printf("PASS: ");
    }
    printf("xyz = %u%u%u, XYZ = %u%u%un", x, y, z, X, Y, Z);
  }
  if (fail != 0) {
    printf("%d TEST FAILED!n", fail);
  } else if (!fail) {
    printf("ALL TEST PASSED!n");
  }
  return 0;
}

三星手机 Java 开发-Hello World 篇

摘要:

这篇文章主要介绍了在 Windows 平台上使用 NetBeans 和 Samsung SDK 构建三星手机 Java 开发环境,建立和调试简单 Java 程序的过程。

目录:

1. 工具
2. 安装和配置开发环境
3. Hello World 程序
4. 在三星模拟器(Samsung Emulator)上调试
5. 结论
6. 参考文献

1. 工具

Windows XP: SamsungSDK 不支持 Windows Vista.

JDK version: 1.6.0_03: 这个版本不重要,自己去下载好了。

NetBeans IDE 6.01(Mobility) Chinese Edition:
http://zh-cn.netbeans.org/download/6.0/ml/netbeans-6.0.1-ml-mobility-windows.exe

Samsung Java SDK 1.0.2
http://developer.samsungmobile.com/Developer/resources/SamsungSDK/SamsungSDK_1.0.2.zip
这个版本的 SamsungSDK 支持的三星手机型号为:
D900 (D600, D820, T809)
E200 (E380, E390, E500, E780, E830, P200, T509)
E250
E590
E740
E790 (E490, E530, E570, E620, E720, E880)
E900 (D800, D830, D840, P900)
F500 (F500, P300, X820, Z370)
J600
L760 (Z140, Z150, Z170, Z230, Z240, Z240E, Z300, Z310, Z360, Z500, ZM60, ZX10, ZX20, ZV40, ZV60)
P310
U100
U600 (U300)
U700 (P910, P920, P940, Z400, Z540, Z560, Z720, ZV50)

2. 安装和配置开发环境

我不知道 JDK 是否必须安装的,因为我电脑里老早就安装上了,但是做 JAVA 开发嘛,JDK 总是需要的,所以建议首先安装 JDK。

安装完 JDK 之后,安装 SUN 公司的 JAVA 集成开发环境 NetBeans。为什么是 NetBeans 而不是 Eclipse?因为 SamsungSDK 官方支持 NetBeans。

当 NetBeans 安装完成后,接着安装 SamsungSDK。需要注意的一点是,由于 SamsungSDK 1.0.2 支持的 NetBeans 版本是 5.5,所以在安装过程中要选择是否安装 NetBeans 插件时,请选择否,因为我们使用的 NetBeans 版本是 6.01。

既然无法自动安装 NetBeans 插件,就需要我们在 NetBeans 中手动添加插件了。具体步骤如下:

一,从 NetBeans 菜单中选择“工具->Java平台”,进入 Java 平台管理器;(我们的 NetBeans 是中文版 *_*,请英文版用户自行理解。)
二,点击“添加平台”按钮,进入“选择平台类型”步骤,选择“Java ME MIDP 平台仿真器”,进入下一步;
三,在“选择要在其中搜索平台的目录”对话框中,选择 SamsungSDK 的安装目录,比如我的是 D:\Program\SamsungSDK,点击确定;
四,这时在“平台文件夹”对话框中会出现三个要检测的平台,将三个平台都勾选上,进入下一步;
五,在“已检测到的平台”对话框中,将三个平台都勾选上,选择“完成”,这样我们的开发环境就配置完成了。

3. Hello World 程序

下面介绍在 Samsung Java 开发环境下如何建立并在模拟器上运行一个 Hello World 项目:

一,“文件->新建项目”,在对话框中项目“类别”选择“Mobility”,然后在右侧“项目”中选择“MIDP 应用程序”,点击下一步;
二,将项目名字更改为“HelloWorld”,项目位置可以自行修改,比如我的就是 D:\J2ME,下面两个“设置为主项目”和“创建 Hello MIDlet”保留为默认值,进入下一步;
三,在“选择缺省平台”对话框中,“仿真器平台”选项选择为“SamsungSDK 1.0”,在设备下拉条中,选择目标平台,比如我的手机是 E908,和 E900 是一类,我就选择“SGH-E900”,其它选项保持不变,进入下一步;
四,在更多配置中用户可以自定义更多配置,刚开始使用可以保持不变,点击完成。

这样我们就建立了一个 Samsung Java 的 Hello World 项目,可以查看它的源代码。由于我们在配置时选择了“创建 Hello MIDlet”选项,此时的源程序已经包含了打印一个“Hello World”的功能,我们只需要在菜单中选择“生成->生成主项目”,即可生成 Hello World 项目。

生成项目后,在项目文件夹里可以找到 dist 目录,比如我的就是 D:\J2ME\HelloWorld\dist,这个目录里面包含了可以在 SGH-E908 上安装并运行的 JAVA 程序:HelloWorld.jar 和 HelloWorld.jad。看着很眼熟吧,对,这和普通的 JAVA 游戏形式是一样的,我们可以把这两个文件下载到手机里并安装它。(不同版本安装方法不一样)

SGH-E908 的安装方法是:先将 jar 和 jad 文件下载到手机的 other files 文件夹中,在待机状态下输入 *#9998*5282#,然后选 3,输入密码 235282,选择 jad 文件安装即可。

安装完程序后,我们可以在自己的 Java world 中找到它,运行后,屏幕上会打印出来一行字符:HelloHello, World! 这样,我们的第一个 Samsung Java 程序就成功了。

4. 在三星模拟器(Samsung Emulator)上调试

当然,我们写嵌入式程序不可能一次成功,总会有需要找 BUG 的时候,那么如何进行调试呢?仍然以我们 Hello World 程序为例:

一,先在程序中设置断点,在要设置断点的行前点击边框即可,比如我在 startApp() 函数中的 initialize (); 一行前设置了一个断点;
二,选择“运行->调试主项目”,就可以开始我们的调试了,耐心等待一会儿,会发现弹出了一个 E908 模拟器的窗口,窗口中写着:Select on to launch,因为只有唯一的一个 HelloMIDlet,所以我们直接选择右下角的 Launch,直接点 Launch 是没用的哦,你要点模拟器的手机键盘,作用和按下手机上某个键是一样的。
三,点下 Launch 之后,激活窗口会重新返回到 NetBeans 中, initialize (); 行前有一个右箭头,指代程序已经运行到断点,我们点“运行->继续”,程序就会继续运行,在模拟器中打印出:HelloHello, World!,并在左下显示 Exit 标志,表示程序运行结束。

如果在第三步产生问题,可能是由于在前面介绍中,我们先“生成主项目”造成的调试信息缺失导致的,可修改设置,更直观的方法是重新建立新项目并跳过生成主项目那一步,使用“运行->运行主项目”或者“运行->调试主项目”来生成主项目文件。

5. 结论

三星手机提供的开发环境在一定程度上还是比较易于使用的,这篇文件对三星手机开发环境的建立做了一个简要的介绍,提供了中文入门导引,降低了中文世界初学者学习的难度。

6. 参考文献

[1] Samsung SDK 1.0.0 Documentation

中科院 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 系统下截图

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)一般都会在这类函数库中实现。

密码学:对3轮DES进行的差分密码攻击

我发现自己做事情老是有虎头蛇尾的习惯,就像文章标题的这篇实验报告一样,本来我的题目是 Differential Cryptanalytic Attacks of Reduced Round DES,后来发现写对更多轮的 DES 攻击代码太困难。其实密码分析很简单,都是别人做过的东西,拿来抄抄就是了,但是实际写起来代码才发现完全不是那么回事。文章中一句话带过的东西可能需要你编码好长时间再加调试,而且还很难找到正确代码做比较,所以写到后来实在泄气,就只写好了一个3轮的攻击,再加上我时间本来就不多,只好作罢。等以后有闲功夫了我再恢复原来那标题吧 :)

鉴于自己在做作业时找代码样例对比的难处,像上次的大作业一样,我把我对 3 轮 DES 差分攻击的实验报告又放到了我的个人网站共享空间里,可以从 https://github.com/solrex/solrex/blob/master/dc_des/dc_des_lr.pdf 下载。这篇文章主要内容是对 3 轮 DES 差分攻击特征的分析和实现,文章里面有全部 DES 加密解密和 3 轮攻击的源代码。本来我觉得我的实验报告够挫的了,因为都是别人已经做出来的东西。后来我发现在 2003 年 8 月份的《计算机工程》杂志上登过这样一篇文章《数据加密标准DES分析及其攻击研究》,基本信息量为0,连引用文献都没有写到 Eli Biham 和 Adi Shamir 经典文章,我庆幸我好歹还附上了代码...希望能对某些对密码学有点兴趣的人有所帮助。

顺便说几句废话,看到 Eric师兄在博客里说 PageRank,我查看了一下我 WordPress 博客的 PageRank,嘿嘿,居然有到 2 ?我怀疑自己是不是看花了眼,在 Win 下的 Firefox Google 工具栏,IE Google 工具栏,Linux 下的 Firefox Google 工具栏查看了三遍,都是显示 2(希望 Google Toolbar 没有耍我)。哈哈,我的博客网站居然也有 PageRank 了!大家以后多到 这里 踩我吧,另外链接我博客时候最好也用这个链接,别用 Space 或者 Blogspot 的博客链接(那个网站是人家的...) :).

基于 WinPcap 的一个嗅探器 Windows

这几天相当之忙,好多事情要做,每天都要熬很晚,觉得都有点儿在崩溃的边缘了。

才写完一个大作业,Windows 下基于 WinPcap 库的图形界面网络嗅探器。以前从来没有摸过 MFC 编程,忽然让写这种程序,都要头痛死了。十一回南京也没时间做,只有回来猛补,从网上抄些,自己写些,拼拼凑凑算是写出来一个很简单的网络嗅探器程序。不过我也不想再花工夫了,其实用 WinPcap 库写嗅探器简单到了极点,最难做的是图形界面,了解 MFC 编程对我价值不是很大,不准备再深入了。我把代码包放到我的网站上了,这里是链接地址:
http://share.solrex.org/MyWork/Eavesdrop_Windows_Sniffer_Based_on_Winpcap_VC6.rar
有兴趣了解如何写网络嗅探器的可以下载下来看看。至于使用网络嗅探器,最好还是用 Ethereal,现在好像改名叫做 Wireshark 了,那才是真正强大的网络嗅探和协议分析工具,我们这种东西都是小儿科。

今天抽空把上一篇博客里给出的在 Linux 下安装 Windows 中文字体的脚本稍微完善了下,也根据师兄的建议把源代码放在了网站上(用 Google translate 访问了一下我的 blogspot 才发现上面显示的代码全乱套了)
http://share.solrex.org/scripts/install_win_CN_fonts_to_linux.sh
,修改了一下上一篇博客内容。之所以喜欢把代码放在文章里主要是经常搜索一些示例代码时发现源代码的下载链接不能使用,还是放在文章里一份拷贝比较有利于知识共享。

估计这个星期还会很忙,有一个对 DES 进行差分攻击的报告,有一个希腊哲学史方面的课程论文,还有很多书和 paper 要看。就算我不兼职,研究生生活也并不轻松啊...

Trace 啊 Trace

一周无事,要实现一个支持双硬件线程的 gdb stub,全是在写 interrupt 和 exception handler ,纯汇编编程,纯 trace 调试,16 和 32 bits 指令夹杂,总会有千奇百怪的问题,机器代码看得眼睛痛苦地要命,下班以后一会儿也不想多呆。有 GDB 用是多么幸福的事情啊,可是我需要给 GDB remote debug 提供支持,好歹算完了,大概只需要解决最后几个 BUG。

回去会看一会儿高达,因为室友喜欢看,而对于我这个定力不足的人来说,计算机书自然没那个有吸引力。还挺刺激,大机器就是男人的玩具啊!唉,下星期我一定要把我的美嘉影城的兑换券给兑了,变形金刚也好哈里波特也好,不然真浪费了。

买了几本书,Design Pattern,Artificial Intelligence,师兄豆列中推荐的。不过还是眼高手低,看完不知要何时了。

周末会出去走走,最近感冒的人挺多,要小心。

至于奥运那档子事,管它一周年也罢,几百天也罢,确定了,不掺和。什么东西到中国总会变味,既然被打上 me generation 的标签,还是 me 下去吧。

慎用const_cast

最近心情很 low,从愤青态度和在百合上和别人讨论问题的情绪化就可以看出来。再加上因为某些原因休息不好,居然在导师给我讲 simulator architecture 的时候感到一阵头晕目眩,差点晕倒,只好请求他坐下给我讲。又该调整自己了。

CPP版上老在讨论错误的程序奇怪输出,而且在公司也做编译器的测试,我也找一些例子小研究一下,这个例子是错误的去掉const属性带来的后果。

Program:
#include <iostream>

using namespace std;

int main()
{
const int a = 1;
int *p = const_cast<int*>(&a);
*p = 2;
cout << "value a=" << a << endl;
cout << "value *p=" << *p << endl;
cout << "address a=" << &a << endl;
cout << "address p=" << p << endl;
return 0;
}

这个程序,从语法上看,没有任何问题(当然,这种用法不提倡 deprecated )。但是输出结果却是这样的:

value a=1
value *p=2
address a=0xbfe9efb4
address p=0xbfe9efb4(depends on machine)

奇怪了,a和p既然指向同一块地址,为什么输出的内容却不一样呢?

在调试的时候发现,在运行完*p=2时,调试器打出的a的地址和p是一样的,而且a和*p的内容都是2。但为什么输出时候会a=1呢?

还是看汇编代码吧,相关部分:
subl $8, %esp
pushl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
subl $12, %esp
//将直接数1压栈
pushl $1
//栈顶指针减12
subl $12, %esp
pushl $.LC0
pushl $_ZSt4cout
.LCFI7:
//这句是"value a="这个字符串的输出函数,char
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
//栈顶指针增加20
addl $20, %esp
pushl %eax
//这句是a的值输出,int
call _ZNSolsEi
addl $20, %esp
pushl %eax
.LCFI8:
//这句是输出的endl
call _ZNSolsEPFRSoS_E
addl $16, %esp

看完之后,有人会问了:怎么没看到a在哪里输出的啊,那么请你注意上面“栈顶指针增加20”这个注释,增加20之后它到哪里了?注意上面就会发现,前面有两次压栈,指针减4*2=8,一次指针减12,然后再加上20,这时候栈顶指针就到了“将直接数1压栈”下面了,显然进入输出函数后,它会从栈中找输出某个值,这里显然是数字1。

这就是为什么会输出a=1了,因为编译器根本没去找a的值,因为a是const int型,编译器认为它的内容不会变,所以干吗非得去内存中找啊,多麻烦,直接把值放到code里输出不就行了!

从某种意义上来说,const 相当于宏定义,但是程序为变量分配了存储空间,这样就可以进行类型检查。所以说很多 C++ 书籍作者提倡用 const 来代替 define,而且反对对 const 做 cast。就算要做 cast,也请尽量使用 C++ 的 cast 而不是 C 的类型转换。

恶劣的编程风格会带来不可预测的后果。类型转换之类的C留下来的习惯在编写高效率的程序时候是很有用的,但一定要确保知道采用这种方式会产生什么结果再去使用。对于那些 undefined 和 deprecated 的东西可以去利用,但是脑子中一定要有 idea。
Copyright © 2005-2006 Solrex Yang. All rights reserved.

strcpy 的 overlap 之迷

在进入正文之前先说几句题外话:最近有个新闻很火,是21岁才女被聘为跨国公司副总裁的事情。且不说新闻内容的真实程度有多少,其实我看到新闻时候就对里面的东西产生怀疑了,但是有几点是无法否认的:人家确实被聘为副总裁了,这点在Topcoder的网站上可以得到证实,至于这个副总裁手下有几个人,和事实没关系;人家确实有过三项发明专利,至于专利内容,和事实也没关系;人家确实是ACM主席成员,至于这个ACM和ACM/ICPC的缩略语的区别,和事实也没有关系;还有人家获得过29届ACM 国际大学生程序设计竞赛亚洲总决赛银牌,至于这个奖牌含金量有多高,和事实也没关系。

这几点已经让我崇拜有加了,Topcoder不是什么太牛的公司,但是起码也不烂;发明专利可能不是很高科技,但是我没有一个;ACM/ICPC主席团没有ACM主席高级,那不是废话吗?谁不知道啊,ACM/ICPC和ACM奖差远了,但是交通大学获奖后不也经常缩写吗?我连校ACM队都进不了,就别歧视人家奖牌了。对于新闻的炒做大家心里都有底,没有必要非得整出来《神话通常是由于低素质产生的》这种无耻文章来做所谓“揭露”,你可以写事实,但不要用这种口吻。还有一些别有用心的人拿所谓高考中考成绩说事,有意思吗?人家不容易。

读心理学专业能把计算机学那么好,还兼职干过那么多事情,能力啊,相比而言真让我受打击!更打击我的是今天在小百合上看到有认识的人说吴莹莹的男朋友是Stanford的王颖(2006 google 全球编程挑战赛亚军)............我只能 keep 无语了。

好了,下面进入正题,今天在小百合上和别人争了一番,争论是争论,总得留下点东西不,不然白费口舌了。

Program 1:
#include <stdlib.h>
int main(void)
{
char a[] = "abcd";
char b[] = "efg";
strcpy(b, "efghi");
printf("a=%s
", a);
printf("b=%s
", b);
return 0;
}
gcc build and run:
a=i
b=efghi

Program 2:
#include <stdlib.h>
int main(void)
{
char a[] = "abcd";
char b[] = "efgh";
strcpy(b, "efghij");
printf("a=%s
", a);
printf("b=%s
", b);
return 0;
}
gcc build and run:
a=abcd
b=efghij

ANSI C/ISO-IEC-9899-1999 在 7.21.2.3 The strcpy function 章节中指出

7.21.2.3 The strcpy function
Synopsis
1 #include <string.h>
char *strcpy(char * restrict s1,
const char * restrict s2);
Description
2 The strcpy function copies the string pointed to by s2 (including the terminating null
character) into the array pointed to by s1. If copying takes place between objects that
overlap, the behavior is undefined.
Returns
3 The strcpy function returns the value of s1.

所以上面的两个程序都有错误,但是我们这里只分析为什么会导致奇怪的输出。

第一个例子,运行时内存的内容是这样的(请注意方括号中的内存单元内容,冒号前是内存地址):
a,b赋完值:
0xbffd7778: 0xbffd7798 [0x00676665] [0x64636261] 0x006dff00

执行完strcpy:
0xbffd7778: 0xbffd7798 [0x68676665] [0x64630069] 0x006dff00

第二个例子:
a,b赋完值:
0xbfef6ca8: 0xbfef6cb8 0x080482b5 [0x68676665] 0x00000000
0xbfef6cb8: 0xbfef6cd8 0x0804843a [0x64636261] 0x006dff00

执行完stycpy:
0xbfef6ca8: 0xbfef6cb8 0x080482b5 [0x68676665 0x00006a69]
0xbfef6cb8: 0xbfef6cd8 0x0804843a [0x64636261] 0x006dff00

奇怪输出的原因找到了,是因为第一个程序把a,b放到连续的空间里导致了覆盖。那么为什么第二个程序中a,b不放到连续空间中呢?

再看看汇编代码,只关注相关部分:

第一个例子:
.LC1:
.string "efg"
...
movl .LC1, %eax
movl %eax, -28(%ebp)
...

第二个例子:
.LC1:
.string "efgh"
...
movl .LC1, %eax
movl %eax, -40(%ebp)
movb .LC1+4, %al
movb %al, -36(%ebp)
...
哦,这时候我们可以看到,由于efg只有三个byte,没有超过计算机的字长,所以eax容纳了它结尾的�,而且可以把b放到一个内存单元(word)中去,所以编译器就把a,b挨着放了。

而efgh有四个byte,一个内存单元(word)不能容纳全部字符串(放不下结尾的�),所以编译器就多为它预留了一段空间,既然留了,就留大点,就是4个word了。程序输出就变成正确的了。

所以就可以看到为什么没有放到连续空间中了。至于这个预留空间的策略,就不得而知了,那得看编译器的逻辑了。
Copyright © 2005-2006 Solrex Yang. All rights reserved.

有趣的 Quine

quine [名字源于逻辑学家 Willard van Orman Quine, via Douglas Hofstadter] ,指的是一种运行后生成自身源代码拷贝的程序,也叫self-reproducing programs(自我复制程序)。今天无意中搜到 Ken Thompson 的一篇经典演讲 Reflections on Trusting Trust,好奇心下就小探索了一番。

answers.com 上有一个c语言的例子:

char*f="char*f=%c%s%c;main(){printf(f,34,f,34,10);}%c";main(){printf(f,34,f,34,10);}

运行之后直觉得好奇,为什么它能把自身源代码输出啊?想着可能是某种特技,结果在Linux下,在Win下编译反编译调试比较汇编代码,折腾了半天。到最后看发现实在太繁,还是静心读一下源代码吧,才发现,原来如此的简单:

printf中的第一个参数f,就是格式化字符串,34就是双引号的"ASCII码,10就是换行符的ASCII码,很显然f就是一个用它自身去格式化自身的字符串而已。技术看起来简单,但是要实现完全相同的代码输出,在编排上还真得下一番工夫。

Gtalk上碰见yutian,给他发了过去,还打赌他肯定看不出来运行结果,现在才发现自己幸亏没真赌上。:D,不过人那,还是感情的动物啊,都有一种习惯性思维,看到貌似复杂的东西自己就先限制住思路了,结果他也没看出来,哈哈。

看来,把复杂的事情搞简单,还真不是件容易的事儿。做人,还是简单点好,遇事想那么多干吗,给自己找烦恼。

PS:

谁要对 quine 还感兴趣,这里 有几乎所有语言的一种 quine 实现,这东东好象对写恶意代码很有用啊。
Copyright © 2005-2006 Solrex Yang. All rights reserved.