Category Archives: Operating Systems

Winsock 发送 HTTP 请求和接收响应

这是一段极其简单的代码,调用 Windows Socket API(Winsock)发送了一段 HTTP 请求,并把相应的 HTTP 响应输出到了标准输出设备上。如果编译时定义了 _UNICODE 或 UNICODE 宏,GetAddrInfo 展开后会成为 GetAddrInfoW,而这是一个新函数,Windows Vista、Windows XP SP2 或 Windows Server 2003 以后的操作系统才支持。实际应用的时候,需要处理各种函数可能导致的错误。HTTP 请求报头也会随着需求改变而不同,这里是向 http://www.66rpg.com:80 发送请求,之后服务器端会发送回正文包含了 66rpg 主页 HTML 文档的 HTTP 响应。BUF_LEN 这个宏指定了每次接收数据用的缓冲区大小,这里使用了一个小数字,方便测试循环的正确性。

#include <stdio.h>
#include <winsock2.h>
#include <Ws2tcpip.h>

#define BUF_LEN 16

void main() {
    WSADATA wsa_data;
    ADDRINFOT *addr_info;
    SOCKET sock;
    char *http_request;
    char buf[BUF_LEN];
    int byte_count;

    WSAStartup(0x0202, &wsa_data);
    GetAddrInfo(TEXT("www.66rpg.com"), TEXT("http"), NULL, &addr_info);

    sock = socket(addr_info->ai_family, addr_info->ai_socktype, addr_info->ai_protocol);
    connect(sock, addr_info->ai_addr, addr_info->ai_addrlen);

    http_request =
        "GET http://www.66rpg.com/ HTTP/1.1\n"
        "Host: www.66rpg.com\n"
        "Connection: close\n"
        "\n";
    printf("%s\n", http_request);
    send(sock, http_request, strlen(http_request), 0);

    do {
        byte_count = recv(sock, buf, BUF_LEN, 0);
        fwrite(buf, 1, byte_count, stdout);
    } while (byte_count > 0);
    printf("\n");

    freeaddrinfo(addr_info);
    closesocket(sock);
    WSACleanup();
}

输出:

GET http://www.66rpg.com/ HTTP/1.1
Host: www.66rpg.com
Connection: close


HTTP/1.1 200 OK
Connection: close
Date: Mon, 27 Dec 2010 20:52:33 GMT
Content-Length: 1299
Content-Type: text/html
Content-Location: http://www.66rpg.com/index.htm
Last-Modified: Tue, 28 Sep 2010 22:28:28 GMT
Accept-Ranges: bytes
ETag: "16641a795c5fcb1:107f6"
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET

<html>
<title>66RPG</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="keywords" content="格斗纹章,柳柳,66RPG,战棋,新战棋,游戏,单机游戏,原
创游戏" />
<head>
<style type="text/css">
<!--
a:link { text-decoration: none;color: blue}
a:active{ text-decoration:blink}
a:hover{ text-decoration:underline; color: red}
a:visited{ text-decoration:none; color:green}
-->
</style>
</head>
<body bgcolor="#DDDDFF">
<p align=center>66RPG</p>
<p align=center>这里只是一个链接汇总,想去哪里,请点击下面的传送门。</p>
<p align=center><a href=http://bbs.66rpg.com/ target="_blank"><font color=red>★
 66RPG 论坛 ★</font></a><br><font color=gray>http://bbs.66rpg.com/</font></p>
<p align=center><a href=http://bbs.66rpg.com/portal.php target="_blank"><font co
lor=red>★ 66RPG 主站(建设中) ★</font></a><br><font color=gray>http://bbs.66r
pg.com/portal.php</font></p>
<p align=center><a href=http://bbs.66rpg.com/thread-153776-1-1.html target="_bla
nk"><font color=red>★ 66RPG 资源帖 ★</font></a><br><font color=gray>http://bbs
.66rpg.com/thread-153776-1-1.html</font></p>
<p align=center><a href=http://www.66rpg.com/GD target="_blank"><font color=red>
★ 格斗纹章 传送门 ★</font></a><br><font color=gray>http://www.66rpg.com/GD</fo
nt></p>
</body>
</html>

Forward Slashes in the Paths Passed to SHFileOperation

Under Windows XP and NTFS, The windows API function SHFileOperation does not seem to be compatible with forward slashes being the directory separators. It also behaves strangely when relative paths are given. According to the Remark section of this MSDN documentation, passing relative paths to this function is not thread-safe. The best way to prevent compatibility issues is to use the good old backward slashes and absolute paths.

Form#submit Failure in Internet Explorer 6

If you are going to submit a form through the anchor tag <a>, watch out for the href attribute. the HTTP response could be rejected by Internet Explorer 6 if you use something like javascript:void(0) as the value of the href attribute. For example, with an anchor tag like this:

<a id="bar" href="javascript:void(0)" onclick="onFoo();">Foo</a>

and with onFoo() handling the form submission (i.e. call submit() on the form object), the HTTP response will not be handled by Internet Explorer 6.

One may suggest replacing javascript:void(0) with #:

<a id="bar" href="#" onclick="onFoo();">Foo</a>

However, this will most likely cause the client to scroll to the top of the page. Instead, use a hash (dummy value) as the anchor name which is not defined anywhere in the document. The client will not scroll up this time.

<a id="bar" href="#doesnotexist" onclick="onFoo();">Foo</a>

Notice that this approach will cause the # sign and the hash to be appended to the URL shown on some clients’ address bars. If you do not like this, add return false; to the onclick event, which prevents the original behavior of the element from functioning (i.e. the hyperlink no longer redirects).

<a id="bar" href="can_be_anything_here" onclick="onFoo(); return false;">Foo</a>

This only works if the onFoo() function does not throw an error.

If the only function you want to implement with the hyperlink is to submit a form (execute onNext()), just get rid of the onclick attribute and go for href.

<a id="bar" href="javascript:onFoo();">Foo</a>

那些和语言有关的坛坛罐罐(一)

从这一篇文章开始,我们来关注在用 C 语言编程时的一些和算法无关、语言有关的、一寸一地的得失问题。这些问题大多数无伤大雅,并不对程序真实效率有多大影响,但可当作学习汇编思想的一种题材。大多数情况下,这些概念和理论能应用于所有语言,但由于编译器和汇编器的实现千姿万态,严谨起见,我们在这里只考虑 ANSI C(C89)标准,并且使用 GNU Compiler Collection(GCC) 4.4.3——一个优化性能极高的编译器,测试环境是 x86-64 Linux(Ubuntu)。

在这一章中,我们来看看每一个 C 程序都会有的内容——程序入口点有什么值得注意的细节。首先我们来编译一个空的源文件:

$ cat > 1.c
$ gcc -O3 -S -masm=intel 1.c

-O3 选项告诉 GCC 我们要打开所有优化模式;-S 选项告诉 GCC 我们要生成汇编代码,而不是可执行文件;-masm=intel 告诉 GCC 生成的汇编语言要使用 Intel 语法,而不是默认的 AT&T 语法;1.c 是我们的源文件名。输出如下:

	.file	"1.c"
	.intel_syntax noprefix
	.ident	"GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
	.section	.note.GNU-stack,"",@progbits

我们可以看到,一个空的 C 源文件经过 GCC 编译后,会生成这样 4 行汇编代码。这 4 行会以不同的形式出现在所有经 GCC 编译的程序代码中,前两行是初始代码,后两行是结束代码。

我们给源文件添加一个 main 函数:

void main() {
}

编译器输出:

	.file	"1.c"
	.intel_syntax noprefix
	.text
	.p2align 4,,15
.globl main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	rep
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
	.section	.note.GNU-stack,"",@progbits

可以看到在初始代码和结束代码之间,又增加了不少代码。我们关心的是 Main 函数内部的代码:

	.cfi_startproc
	rep
	ret
	.cfi_endproc

.cfi_startproc 和 .cfi_endproc 分别是 dwarf2 CFI 的初始过程和结束过程指令,它们隐藏了一些 CFI 有关的操作。rep 之所以出现在这里是因为 GCC 尝试避免分歧跳转跟着 ret 指令时发生 CPU 预测器无法得知 ret 的目标地址的问题(这个问题实际只存在于 x86-64)。ret 是从当前过程中返回的指令。这就是一个最简单的 main 函数内部的三个步骤:CFI 初始操作 – 返回 – CFI 结束操作。由于第一个和最后一个步骤永远伴随着函数,我们大可将注意力集中在这两个步骤之间的代码,也就是 main 函数的实际内容。

现在我们让 main 函数变成更规范、我们更熟悉的形式:

int main() {
    return 0;
}

这时,我们之前提到的第一个步骤和第二个步骤之间的代码就变成了:

	xor	eax, eax
	ret

我们可以看到,生成的代码比之前多了一行。eax 是一个通用的寄存器,根据 cdesl 调用约定(即 C 语言调用约定),在函数返回时,返回值必须保存在 eax 寄存器中,交给调用者处理。xor 是异或运算指令,自己异或自己并保存结果到自己,自己自然就成了 0,这是在汇编层把一个寄存器设为 0 的最快的方法(它的操作码在很多平台上都比 mov eax, 0 指令短)。所以,当 main 函数有返回值时,程序的代码是比没有时长的。

我们再给 main 函数加上常见的参数列表:

int main(int argc, char **argv, char **envp) {
    return 0;
}

输出后你会发现汇编代码与之前相比没有改变。如果我们把 return 0 改为:

    return argc;

输出就改变了:

	mov	eax, edi
	ret

edi 是保存了 argc 的寄存器,mov 使 edi 的值传送到了 eax,实现了 return argc 的功能。由此可见,main 参数的传递是由调用者完成的(在这里 main 函数的调用者是装载器),并没有影响程序代码长度。

  我们现在去掉 GCC 的 -O3 选项,即不进行任何优化,编译带三个参数、返回 argc 的 main 函数代码,发现输出为:

	push	rbp
	.cfi_def_cfa_offset 16
	mov	rbp, rsp
	.cfi_offset 6, -16
	.cfi_def_cfa_register 6
	mov	DWORD PTR [rbp-4], edi
	mov	QWORD PTR [rbp-16], rsi
	mov	QWORD PTR [rbp-24], rdx
	mov	eax, DWORD PTR [rbp-4]
	leave
	ret

你一定震撼了。多出来的这些代码,是函数调用和返回时的常规代码,进行栈指针偏移、局部变量入栈等操作。由于我们这段代码根本没有必要使用栈内存(没有调用其它函数),GCC 在优化的时候就把我们的程序当作了一串没有非局部跳转的指令,省略了这些操作,直接返回,达到了未优化时的同样效果。这是一种最常见的编译器优化技术——死代码消除法(dead code elimination),根据上下文排除了不必要、不会影响程序运行的代码。这里只是死代码消除法的冰山一角而已。

好了,关于程序入口点,差不多就是这些内容。我们可以在不需要告诉操作系统程序运行的结果时让 main 函数不返回任何值,虽然并不规范,但确实无伤大雅,节省了一行汇编代码。同时,也不要担心写入了形式参数会增大生成的程序体积的问题——编译器强大的优化在很多时候比人类刻意去写的所谓“优化”后的汇编代码更优,这种简单的“死代码消除”对它来说实在是小菜一碟,它会在运行速度和代码体积之间进行合理的折衷(也可以通过用户传递的选项手动设置)。

[RMXP] Hangup 异常根除

通用 FSL 信息

脚本说明

灼眼的夏娜曾经发布过一个解决 RMXP 10 秒不刷新就抛出 Hangup 异常,提示“脚本已备份”(这个错误信息是汉化失误)问题的脚本,其原理是创建一个 Ruby 线程,定期调用 Graphics.update,这样就能防止异常的抛出。这个解决方法有一个弊端,就是当 RM 进程阻塞时,Ruby 线程也会停止运行,超过 10 秒后仍如果进程从阻塞中恢复,仍然会抛出 Hangup 异常。我们自然而然地会想:难道不能从根本杜绝 Hangup 的抛出吗?

前不久在写精确获取窗口句柄的时候,发现 RMXP 游戏进程创建了不止一个的 Windows 线程,通过一个一个地结束线程发现,游戏进程最后创建的一个线程恰好就是控制 Hangup 异常抛出的线程。估计 10 秒的计时也是在这个线程内部的用户代码中进行的。那我们简单地、暴力地把这个线程咔嚓掉,不就搞定了吗?我这么做了,结果发现:原来处理程序最终化(即退出时)的也是这个线程,在我把这个线程咔嚓之后,无论是点窗口右上角的关闭按钮,所有脚本都解释完毕,还是在异常抛出到 Ruby 顶层后的正常退出都失效了,只能通过结束进程来关闭游戏。

想了几天,终于想到了解决方法:在程序初始化的时候就暂停掉这个线程,直到游戏需要退出的时候再恢复其用户代码的运行,这样,游戏过程中所有本来应该抛出 Hangup 异常的地方都被鞋盒掉了,这就是这个脚本的暴力之处。但关键就是——怎么在所有需要退出的场合下都进行线程的恢复?点窗口关闭按钮的场合很容易,可以重新定义 exit;所有脚本解释完毕的场合也很容易,可以在 main 脚本下面做相关的处理;而异常抛出到顶层的场合,似乎就不那么么简单了。你说可以在 main 脚本的 begin … rescue … end 那里捕获其它所有异常?那万一在前面定义 Game_Temp 啊 Scene_Title 啊之类的地方发生了异常(也就是解释到 main 脚本之前)怎么办呢?

现在的办法是这样的:游戏运行的时候,RM 会把脚本读取到一个全局数组 $RGSS_SCRIPTS 中,而实际在解释脚本的时候也是在访问这个数组的内容。我们可以越俎代庖,在 RM 解释所有脚本之前(不包括越俎代庖的这个脚本)先把脚本解释完了,然后直接退出程序。换句话说,就是通过 Ruby 的 eval 函数去代替了 RM 的 RGSSEval 函数,解释了所有的脚本。这样一来,在解释脚本过程中所有的异常(当然也不包括越俎代庖脚本中的异常,下面发布是经过调试后的脚本,应该不会有错误了,不过为了保险起见还望大家多多测试)都可以通过 rescue 来捕获到了!当然有利也有弊,这样做了之后,当异常抛出时,提示的消息就只有“eval:#{出错行数}#{出错信息}”,而没有为用户提供脚本的标题,并在脚本编辑器中把光标指向出错的地方了,这是这个脚本的副作用。

当然,发生错误的时候你完全可以暂时屏蔽掉这个脚本,这样就可以把异常后的效果恢复到从前那样了,反正调试的时候你也不一定需要这个脚本。另外,这个脚本在捕获到异常,恢复了线程的运行之后就直接把异常再次抛出给了顶层,所以异常错误信息没有脚本的标题。但其实在循环中,完全可以通过 $RGSS_SCRIPT[subscript] 的第二个元素获取到当前的脚本标题,有心人可以自己改一下,让脚本弹出消息框告诉你这些信息,而不是直接交给 RM 的异常处理机制来处理。

最后,这个脚本完全有可能引起未知的问题,因为被屏蔽掉的线程对我们普通 RM 用户来说还是未知的,万一它还处理了什么其它的东西呢?我们还需要更多的测试。

更新历史

  • 1.2.0827 By 紫苏

    • 更改了配置模块名
    • 更改了 FSL 注释信息
  • 1.2.0805 By 紫苏

    • 脚本开始遵循 FSL
    • 全局范围内改变了脚本结构
  • 1.1.1101 By 紫苏

    • 修正了脚本在 Windows XP 平台下失效的问题
  • 1.0.0927 By 紫苏

    • 初始版本完成

脚本源码

#==============================================================================
# ■  Hangup 异常根除
#    Hangup Exception Eradication
#----------------------------------------------------------------------------
#
#    Hangup 异常是 RMXP 底层引擎内置的一个异常类,游戏进程会在 Graphics.update
#    没有调用超过 10 秒时抛出这个异常。这个脚本使用了 Windows API 暴力地解除
#    了这个限制。
#    使用方法:Hangup 异常根除脚本必须插入到脚本编辑器的最顶端,所有脚本之前,无
#    例外。
#
#----------------------------------------------------------------------------
#
#    更新作者: 紫苏
#    许可协议: FSL -MEE
#    项目版本: 1.2.0827
#    引用网址: 
#    http://bbs.66rpg.com/forum.php?mod=viewthread&tid=134316
#    https://szsu.wordpress.com/2010/08/09/hangup_eradication
#
#----------------------------------------------------------------------------
#
#    - 1.2.0827 By 紫苏
#      * 更改了配置模块名
#      * 更改了 FSL 注释信息
#
#    - 1.2.0805 By 紫苏
#      * 脚本开始遵循 FSL
#      * 全局范围内改变了脚本结构
#
#    - 1.1.1101 By 紫苏
#      * 修正了脚本在 Windows XP 平台下失效的问题
#
#    - 1.0.0927 By 紫苏
#      * 初始版本完成
#
#==============================================================================

$__jmp_here.call if $__jmp_here

#----------------------------------------------------------------------------
# ● 登记 FSL。
#----------------------------------------------------------------------------
$fscript = {} if !$fscript
$fscript['HangupEradication'] = '1.2.0827'

#==============================================================================
# ■ FSL
#------------------------------------------------------------------------------
#  自由RGSS脚本通用公开协议的功能模块。
#==============================================================================

module FSL
  module HangupEradication
    #------------------------------------------------------------------------
    # ● 定义需要的 Windows API。
    #------------------------------------------------------------------------
    OpenThread = Win32API.new('kernel32', 'OpenThread', 'LIL', 'L')
    CloseHandle = Win32API.new('kernel32', 'CloseHandle', 'L', 'I')
    Thread32Next = Win32API.new('kernel32', 'Thread32Next', 'LP', 'I')
    ResumeThread = Win32API.new('kernel32', 'ResumeThread', 'L', 'L')
    SuspendThread = Win32API.new('kernel32', 'SuspendThread', 'L', 'L')
    Thread32First = Win32API.new('kernel32', 'Thread32First', 'LP', 'I')
    GetCurrentProcessId = Win32API.new('kernel32', 'GetCurrentProcessId', 'V', 'L')
    CreateToolhelp32Snapshot = Win32API.new('kernel32', 'CreateToolhelp32Snapshot', 'LL', 'L')
  end
end

#==============================================================================
# ■ HangupEradication
#------------------------------------------------------------------------------
#  处理根除 Hangup 异常的类。
#==============================================================================

class HangupEradication
  include FSL::HangupEradication
  #--------------------------------------------------------------------------
  # ● 初始化对像。
  #--------------------------------------------------------------------------
  def initialize
    @hSnapShot = CreateToolhelp32Snapshot.call(4, 0)
    @hLastThread = OpenThread.call(2, 0, self.getLastThreadId)
    #@hLastThread = OpenThread.call(2097151, 0, threadID)
    ObjectSpace.define_finalizer(self, self.method(:finalize))
  end
  #--------------------------------------------------------------------------
  # ● 获取当前进程创建的最后一个线程的标识。
  #--------------------------------------------------------------------------
  def getLastThreadId
    threadEntry = [28, 0, 0, 0, 0, 0, 0].pack("L*")
    threadId = 0                                          # 线程标识
    found = Thread32First.call(@hSnapShot, threadEntry)   # 准备枚举线程
    while found != 0
      arrThreadEntry = threadEntry.unpack("L*")           # 线程数据解包
      if arrThreadEntry[3] == GetCurrentProcessId.call    # 匹配进程标识
        threadId = arrThreadEntry[2]                      # 记录线程标识
      end
      found = Thread32Next.call(@hSnapShot, threadEntry)  # 下一个线程
    end
    return threadId
  end
  #--------------------------------------------------------------------------
  # ● 根除 Hangup 异常。
  #     2       : “暂停和恢复线程访问权限”代码;
  #     2097151 : “所有可能的访问权限”代码(Windows XP 平台下无效)。
  #--------------------------------------------------------------------------
  def eradicate
    SuspendThread.call(@hLastThread)
  end
  #--------------------------------------------------------------------------
  # ● 恢复 Hangup 异常。
  #--------------------------------------------------------------------------
  def resume
    while ResumeThread.call(@hLastThread) > 1; end        # 恢复最后一个线程
  end
  #--------------------------------------------------------------------------
  # ● 最终化对像。
  #--------------------------------------------------------------------------
  def finalize
    CloseHandle.call(@hSnapShot)
    CloseHandle.call(@hLastThread)
  end
end

hangupEradication = HangupEradication.new
hangupEradication.eradicate

callcc { |$__jmp_here| }                                  # F12 后的跳转标记

#==============================================================================
# ■ 游戏主过程
#------------------------------------------------------------------------------
#  游戏脚本的解释从这个外壳开始。
#==============================================================================

for subscript in 1...$RGSS_SCRIPTS.size
  begin
    eval(Zlib::Inflate.inflate($RGSS_SCRIPTS[subscript][2]))
  rescue Exception => ex
    # 异常发生并抛出给解释器时恢复线程。
    hangupEradication.resume unless defined?(Reset) and ex.class == Reset
    raise ex
  end
end

hangupEradication.resume
exit

已知 BUG 与冲突

  • 脚本在 Windows XP 平台下失效(已修复)