IDEA下建立Forge开发环境的正确姿势

前言

本文作于2014年末,其中记载的方法可能已经过期,请读者谨慎参考
见过不少教程都是基于Eclipse的,而基于IDEA的文章少得可怜,遂决定写此文。
本文通篇基于Linux/IntellijIDEA进行讲解,Windows/MAC/Eclipse用户请自行依葫芦画瓢。

设置Forge工作区

当然,你得首先去MinecraftForge下载一份源代码。我这里用的是最新的forge-1.7.10-10.13.2.1258-src.zip
接着,找个地方建立一个文件夹,这将是你的工程目录,我的叫做Forge1.7.10-1258。然后再在里面建立一个目录,比方说就叫forge-1.7.10-10.13.2.1258-src,把你的Forge源码解压进去。
现在,你的文件夹层次应该看起来是这样的:

Forge1.7.10-1258
└── forge-1.7.10-10.13.2.1258-src
     ├── build.gradle
     ├── CREDITS-fml.txt
     ├── eclipse
     ├── forge-1.7.10-10.13.2.1258-changelog.txt
     ├── gradle
     ├── gradlew
     ├── gradlew.bat
     ├── LICENSE-fml.txt
     ├── MinecraftForge-Credits.txt
     ├── MinecraftForge-License.txt
     ├── README.txt
     └── src

你可以先按自己喜好改动一下build.gradle。比如,我喜欢手动指定一下mappings的版本:

1
2
3
4
5
minecraft {
version = "1.7.10-10.13.2.1258"
runDir = "eclipse"
mappings = "stable_12"
}

接着,cd到forge-1.7.10-10.13.2.1258-src目录下,执行如下两条命令:

gradle -i setupDecompWorkspace
gradle -i ideaModule

然后请耐心等待指令完成,可以去喝杯牛奶睡个觉什么的。

Minecraft Coremod开发杂事记

表示最近时间荒废得厉害,主要都是耗在了Minecraft这款游戏上。
Minecraft的一大魅力在于其几乎无穷的MODs,于是我也小试了一下Mod开发,顺便学习一下Java。于是掉入了万劫不复的深坑
当然,我要做点和加个方块、改个合成表之类的不一样的事。
(教程中不少内容都参考了szszss的博客,在此表示深深的感谢)

基础知识

关于这篇文章,不适合特别特别新的新人,我假设各位读者都有一些基础的编程经验。如果你是入门级别的,在MCBBS论坛的编程开发板块有不少不错的入门教程。
我假设各位读者都具备以下能力:

  • 会安装软件
  • 了解基本的程序流程控制,比如判断、循环等
  • 了解基本的OOP概念,比如类,继承等 (其实这条不是那么重要,Java看多了就自然会了 一个原C程序员如是说
  • 了解命令行、终端的基本使用方法
  • 有方法正常访问国际互联网,如Facebook等
  • 了解基本英语单词(这条似乎也不是那么重要,主要是希望大家能够在遇到问题时不要怕阅读英文资料)
  • (本教程面向Linux用户,Mac用户大同小异,Windows用户自己看着办)

域名注册商更换

昨天折腾了一天,把域名从Godaddy转移到了Name.com
表示基本没遇到什么麻烦,信用卡借用了家长的,付款也很方便。
关键是便宜啊。转入9美刀,续期11美刀,比Godaddy坑爹的18刀便宜太多了啊~
而且还有免费的WHOIS保护啊
优惠码PRIVACYPLEASE超好记有木有!
虽然不是最便宜的但是Name.com的控制台相当美观呐~
结尾吐槽一句:在万网注册的都是真的勇士。

Programming with PTRACE, Part6 - 时间控制

不同的时间计算方法

程序运行会占用一小段时间(废话),事实上,我们有不止一种方法来表示一个程序运行了多长时间。最直观的应该是“墙上时间”,也就是说,你掐个秒表,看看程序从开始到结束用了多长时间。除此之外,还有“用户态时间”和“内核态时间”,这两个时间都是以CPU实际运算的时间,也就是CPU周期,来计数的。“用户态时间”就是程序在用户态执行的时间,包括程序所引用的库中的代码(比如STL),“内核态时间”就是指程序在内核态执行的时间,一般是各种系统调用(比如各种IO操作)。这两种时间和墙上时间的区别在于,因为CPU其实是在多个程序中快速切换的,所以在运行某个程序的时间里,CPU也处理了属于其他进程的任务,而且CPU切换任务也需要一定的时间(真的很短)。如果处于被调试状态,tracer的运行时间也会被计算在内,这些不属于这个进程的时间片也会被计算在这个进程的“墙上时间”里。所以一般以用户态时间和内核态时间的总和作为进程的运行时间。

在Linux系统里有一个叫time的命令可以查看一个命令执行了多长时间。这个命令有两个版本,一个是shell内置的,另一个是独立的可执行文件,可以用type time命令查看。虽然可执行版本功能更强一点,但内置的功能足够,这一点区别可以不管。用法是: time [命令] <参数>。给个例子:

time ffmpeg -i sample.mp4 target.mp3
...
5.42s user
0.10s system
100% cpu
5.520 total

Programming with PTRACE, Part5 - 内存管理

这个part主要讲解Linux的内存管理机制,以及如何查看并限制子进程的内存使用。

内存的划分

(嘛。。。这一部分也算是现学现卖的,如果大家觉得有什么讲的不到位的请翻下方的拓展阅读部分)
大家都知道,32位系统最大可以寻址4GB的地址空间(不考虑物理地址扩展),那么这个“地址”究竟指的是哪儿的地址呢?你可以写一个小程序,malloc一点内存,然后把地址打印出来,重复几次,你会发现,分配的内存几乎都在同一个位置。这是因为,对于程序来说,这些地址都是虚拟地址,虚拟地址空间对于每个进程都是独立的,也就是说,对于不同的进程,同样虚拟地址上的数据是不同的。
当然,数据肯定是存放在内存条上的,我们把可以直接读写内存条的地址叫做物理地址。物理地址以一定的方式映射到虚拟地址上,所以当程序试图访问虚拟地址时,系统要以一定方式把虚拟地址变成物理地址,这项工作通常是由MMU(内存管理单元)来完成的。内存的映射不是大块大块的,而是一小片一小片分别映射的,所以在虚拟地址上连续的地址可能在物理地址上相差十万八千里,这些一小片一小片的内存被称为“页”。
页的存在给内存分配带来了极大的灵活性,页可以存储在内存里,也可以存储在交换分区里,可以将同一块物理内存映射到不同进程的虚拟空间里(动态库经常这么干),甚至可以映射到磁盘上的某个文件。光说可能有点抽象,于是给幅图(来自Wikipedia)
内存页映射是不连续的
虚拟地址被分成多个段,数据有序存放于其中。这是32位Linux的新内存布局(Linux 2.6.7之后):
Linux新内存布局
如果你研究过可执行文件的结构,你就会发现,虚拟地址的段就是按可执行文件的段来填充的。另外,由于代码段的起点地址是固定的(0x08048000),所以编译器就可以预先算出函数的地址了。顺带一提,因为动态库加载时的虚拟地址是不固定的,不能预先计算出函数地址,所以要在编译时使用-fPIC选项生成位置无关代码,否则每次被一个新进程使用时都要进行重定位(可以理解为重新计算函数地址),并生成该动态库的一个副本,这样压根没有起到节约内存的作用。
扯远了,回来。尽管每个进程的虚拟地址空间时互相独立的,但并不意味着进程想访问哪儿就能访问的,比如3GB以上的区域,那儿是内核的领地。即使是堆段,也只能访问已申请的内存部分,非法的内存访问将会引发段错误(Segmentation Fault)。回到malloc()函数上,malloc最终会调用brkmmap系统调用,brk用于在堆中分配小块内存,mmap则用于在Memory Mapping Segment中分配大块内存。但是并不是每次malloc都会调用brk,这是因为分配的内存实在是太小了,而brk只能分配大一点的内存,所以C运行库(比如glibc)在收到一个malloc时会先用brk向系统“批发”一块大一点的内存,而收到后续分配请求时则把这块大内存“零售”给程序,直到售完再次brk。
如果有一个程序死循环单纯malloc内存,内存会不会被吃光呢?答案是不会(我不清楚是不是真的有如此单纯的系统真的会挂掉),因为系统发现,你只是分配了内存,却没有使用,于是它很机智地将那片内存设置为“可访问”,却没有把它映射到任何一个实际的内存页上!

Programming with PTRACE, Part4 - 系统调用进阶

这个part是Part2的延续,所以我强烈建议你弄明白Part2中的内容后再来看本part。那么进入正题,我将在这个部分讲解系统调用的参数传递顺序以及如何利用ptrace系统调用获得用户空间的数据。

参数与寄存器

我在Part2中提到过,系统调用的参数是以一定顺序保存在寄存器里的,那么这个顺序是什么呢?在man 2 syscall中有两张表格解释了这个问题,你也可以在这里看到,就在”Architecture calling conventions”下面。我知道很多人很懒,所以我就把这两张表格复制过来了。

arch/ABI instruction syscall # retval Notes
arm/OABI swi NR - a1 NR is syscall #
arm/EABI swi 0x0 r7 r0
blackfin excpt 0x0 P0 R0
i386 int $0x80 eax eax
ia64 break 0x100000 r15 r10/r8 bool error/errno value
parisc ble 0x100(%sr2, %r0) r20 r28
s390 svc 0 r1 r2 See below
s390x svc 0 r1 r2 See below
sparc/32 t 0x10 g1 o0
sparc/64 t 0x6d g1 o0
x86_64 syscall rax rax
arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7
arm/OABI a1 a2 a3 a4 v1 v2 v3
arm/EABI r0 r1 r2 r3 r4 r5 r6
blackfin R0 R1 R2 R3 R4 R5 -
i386 ebx ecx edx esi edi ebp -
ia64 out0 out1 out2 out3 out4 out5 -
parisc r26 r25 r24 r23 r22 r21 -
s390 r2 r3 r4 r5 r6 r7 -
s390x r2 r3 r4 r5 r6 r7 -
sparc/32 o0 o1 o2 o3 o4 o5 -
sparc/64 o0 o1 o2 o3 o4 o5 -
x86_64 rdi rsi rdx r10 r8 r9 -

Programming with PTRACE, Part3 - 进程的终止与信号

在Part2中,我们粗略了解了如何使用ptrace获得系统调用信息,即在一个大循环里不断获取程序信息,如果程序退出则停止循环。当然,那个判断异常简陋,几乎无法处理任何特殊情况。我将在本Part中详细解说各种异常情况的处理,同时讲解各种信号相关的问题。

一些重要的宏

在使用wait4后,程序的信息被存储在sta变量中,这些信息被存储在这个整数的不同二进制位上,这儿有一系列宏用于帮我们提取这些信息。以下信息是我对man 3 wait中相关部分的翻译,同时参考了这个页面

WIFEXITED   如果进程正常退出,返回一个非0值(通常是进程调用了`exit()`或是`_exit()`)
WIFSIGNALED 如果进程由于一个未被捕获的信号而被终止,返回一个非0值
WIFSTOPPED  当进程被停止(非终止)时,返回一个非0值(通常发生在当进程处于`traced`状态时)

WEXITSTATUS 当`WIFEXITED`为非0值,获得进程`main()`函数的返回值
WTERMSIG    如果`WIFSIGNALED`为非0值,获得引起进程终止的信号代码
WSTOPSIG    如果`WIFSTOPPED`为非0值,获得引起进程停止的信号代码

除了这六个,还有WIFCONTINUEDWCOREDUMP两个宏,不过我们用不到,我也没仔细研究,就不说了。
当进程自行终止时,WIFEXITED即为true,配套使用WEXITSTATUS获得返回值,不做过多解释。当子进程进行系统调用时,WIFSTOPPEDtrue,同时WSTOPSIG等于SIGTRAP(信号代码为7),我们可以用这种方法区分syscall-stopsignal-delivery-stop。当有一个外部信号要发送给子进程,这个信号会先到达父进程,使WIFSTOPPEDtrue,同时WSTOPSIG等于该信号的信号代码。父进程可以选择将这个信号继续传递或是不传递,甚至传递另一个信号给子进程。一旦信号真正到达子进程,就进入子进程自己的处理流程或是系统默认动作,可能触发WIFSIGNALED,比如SIGINT
在所有信号中,SIGKILL是一个例外,它不会经过父进程引发WIFSTOPPED,而是直接传递到子进程,引发WIFSIGNALED