谈谈Unity的资源管理

Unity最佳实践明确指出, 要使用AssetBundle而不是Resources目录来管理资源。

然而,事情并不像Unity官方描述的那么美好。因为使用AssetBundle我们甚至无法实现一个高效易用的,完全自动化资源管理方案。

据Unity官方说,一般有两种方案。

方案一,如果你的游戏是关卡性质的,可以在一个关卡里加载所有AssetBundle,然后在进入下一关卡时,卸载本关卡中加载的所有AssetBundle. 但这种机制似乎只对愤怒的小鸟这种小游戏才适用吧:D。

方案二,如果你的游戏不是关卡类的,那么Unity推荐做一个资源对AssetBundle引用计数。

如果一个对象(Asset或其他AssetBundle)引用此AssetBundle则其引用计数加1. 如果此AssetBundle首次加载(即加载前引用计数为0), 还需要递归对其依赖引用计数加1。

如果一个AssetBundle的引用计数为0则释放这个AssetBundle,同时还需要递归对其依赖引用计数减1.

除非,我们做像愤怒小鸟一样的通关游戏,不然似乎只有方案二给我们用。而且方案二乍一看是完备的,因为这正是GC算法的一种实现。

但是如果稍微仔细思考一下就会发现,这个方案只是AssetBundle的管理方案,是个半成品,要如何管理资源之间的依赖,Unity却只字未掉,看起来是让用户自己想办法,这似乎与其易学易用的宗旨不太相符。

下面来分析一下Unity中资源之间的关系。

在Unity中资源大约分为以下几种:

纹理(Texture)、网格(Mesh)、动画片段(AnimationClip)、音频片段(AudioClip)、材质(Material)、着色器(Shader)、字体资源(Font)以及文本资源(TextAsset)。

AssetBundle中还有一个极其特殊的存在,那就是Prefab, AssetBundle.LoadAsset时返回的是GameObject, 但是又必须经过Instantitate之后变成另外一个GameObject才能使用。此后所说的GameObject均是Instantitate之后的GameObject。

GameObject可以添加各种Component来引用上述资源,还可以通过代码动态增减某个GameObject上的Component或者修改Component对资源的引用。这种灵活性给资源管理带来了巨大麻烦,而没有这种灵活性,逻辑的实现就会更麻烦。


下面,举例来说明一下,要正确管理GameObject和资源之间的引用关系有多么艰难。

Prefab P能过Instantitate生成A,B,C,D四个GameObject.

执行如下代码之后,A引用{P,T1}, B引用{P,T1}, C引用{P,T3}。并且T2应该被Unload。

1: A.GetComponent<SpriteRender>().sprite = (Sprite)T1;
2: B.GetComponent<SpriteRender>().sprite = (Sprite)T1;
3: C.GetComponent<SpriteRender>().sprite = (Sprite)T2;
4: C.GetComponent<SpriteRender>().sprite = (Sprite)T3;

要想自动正确的管理GameObject和资源的引用关系,就必须要感知到对GameObject的赋值操作。

例如:所有的sprite赋值都必须使用类似SpriteAssign(SpriteRender sr, Sprite s)的接口。

SpriteAssign的执行流程通常是这样的。

  1. 检查sprite的值是不是T1相同,如果是相同则不做处理
  2. 检查sprite的值是不是从P中clone过来的,如果不是,将此sprite的引用计数减1
  3. 将T1的引用计数加1

如果P是一个树状态结构,即有P–(child)–>p1–(child)–>p2。

1: A.p1.p2.GetComponent<SpriteRender>().sprite = (Sprite)T1;
2: B.p1.p2.GetComponent<SpriteRender>().sprite = (Sprite)T1;
3: C.p1.p2.GetComponent<SpriteRender>().sprite = (Sprite)T2;
4: C.p1.p2.GetComponent<SpriteRender>().sprite = (Sprite)T3;

SpriteAssign接口中的步骤2就显得格外复杂,它必须修正引用关系如下:A引用{P,T1}, B引用{P,T1}, C引用{P,T3}。

同时Destory操作也要被感知,如果Destory(A)则需要释放A引用的资源,而如果Destory(A.p1.p2)则需要修正A对资源的引用情况。因为此时的引用关系是,A引用{P}。换句话说Destroy的开销也会变大。

而赋值和Destory都算不上低频操作,尤其是赋值操作。这样的开销已经足够让程序慢上好几倍了。如果不能承受这些开销,全自动化资源管理是不可能实现的。

我想这也是Unity不默认提供一套标准的全自动化资源管理方案,而是让用户根据实际情况来自己做trade-off的根本原因吧。


受方案一的启发,我觉得可以通过如下接口做一个半自动化的资源管理器。

void frame.record(asset);
void frame.dispose();
void stack.open(frame f);
frame stack.close();
void stack.push() {
frame l = new frame()
stack.open(l)
}
void stack.pop() {
frame f = stack.close()
f.dispose()
}

每一个frame对象都会记录在stack.open()和stack.close()之间所有加载过的资源,加过载多少次就记录多少次,这些资源会在执行frame.dispose()时如实的进行释放。

其中stack在管理栈式UI资源方面几乎已经达到了全自动化,当你打开一个UI时调用stack.push,在退出此UI时调用stack.pop会自动释放在此UI期间你所加载的全部资源。

而在其他不具有栈式加载资源特征的地方,frame类也提供了一种方便的半自动化管理方案。在加载资源之前调用stack.open(frame),在加载资源之后调用stack.close()。

最重要的是,此种方案的开销和复杂度,都要远低于全自动化管理方案。

一次git事故

在开发过程中,我们的版本管理方式是,一版本一分支。之所以这样,是因为随着开发的进行,版本随时会分叉(这里分叉是指,两个版本都需要后续开发,但是对同一模块的功能需要不一样的微调)。

随着分叉之后的持续开发,就会导致多个分支之间差别越来越大。因此,如果所有版本都添加同样功能或修改同一个Bug时,是不可以采用git merge的方式来进行的。因为很难找到他相同的父节点。这时patch会是一个很好的工具。

而这次事故正是git patch功能使用不当引起的,下面来模拟出一个完整的事故现场。

在最开始我们有一段原始代码如下,可以明显看到,在第18行代码中,将dst错打成了src。

void foo(std::vector<int> &src, std::vector<int> &dst, int id)
{
	for (auto iter = src.begin(); iter != src.end(); )
	{
		if (*iter == id)
		{
			iter = src.erase(iter);
		}
		else
		{
			++iter;
		}
	}
	for (auto iter = dst.begin(); iter != dst.end(); )
	{
		if (*iter == id)
		{
			iter = src.erase(iter);
		}
		else
		{
			++iter;
		}
	}
	dst.push_back(id);
	return ;
}

以此次commit为节点,分切出分支ver1和ver2。

然后在ver1分支上修改上述错误并使用git commit提交,由于也许有很多并行版本都有这个问题,一个一个去改很消耗时间还容易错。因此可以使用`git formatch-patch HEAD~1`来制作一个patch文件,我们得到的patch文件内容如下:

From 3c3a434d131c8cea38eb77b7b0f4681b78ae4172 Mon Sep 17 00:00:00 2001
From: findstr <findstr@sina.com>
Date: Sun, 4 Mar 2018 15:43:43 +0800
Subject: [PATCH] bugfix

---
 a.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/a.c b/a.c
index 241c07f..1157ab4 100644
--- a/a.c
+++ b/a.c
@@ -15,7 +15,7 @@ void foo(std::vector<int> &src, std::vector<int> &dst, int id)
 	{
 		if (*iter == id)
 		{
-			iter = src.erase(iter);
+			iter = dst.erase(iter);
 		}
 		else
 		{
--
2.4.6

切到ver2分支后,先手动将18行修改提交一次,再使用`git am 0001-bugfix.patch`来重复修复这个Bug。

预期中,执行完git am之后,只有两种情况。一是这个文件完全没有变化,二是会出现conflict。

然而,事实并没有想象中的那么美好,执行完git am之后,第7行的src被改成了dst。

以上基本就是整个事故的全部还原过程。

这种事故很难发现,一旦发现却很容易就知道原因。

分析一下patch文件,就会立即发现,整个patch行为是靠以下7行代码来定位的。

 	{
 		if (*iter == id)
 		{
-			iter = src.erase(iter);
+			iter = dst.erase(iter);
 		}
 		else
 		{

恰巧,由于在git am之前,我手动修改了这一行,导致在执行git am时,不可能会匹配到这一块出错的代码。更巧的是,git可以使用个patch中的代码块匹配到第4行的代码,即然匹配成功了,git自然就会将修改应用。

从这个事故中,可以得到几点启示。

1. patch并不是100%可靠的,执行之后最好查看一下结果
2. 大扩号另起一行,对版本管理工具不友好:D
3. DRY原则 不但对人适用,对版本管理工具同样适用

ps. 如果实在没有办法,就尽量扩大git diff输出的代码块大小,`git config –global diff.context 30`即可把代码块增加致30行

C程序中让两个不同版本的库共存

今天有同学提出,如何在一个C程序中让两个不同版本的库共存。

首先想到的方案是,把其中一个版本的库函数全部重命名,比如把每一个函数名都加一个_v2的后缀。

人工替换到没什么,但是如果函数个数超过10个,就有点不拿人当人使了。

而使有工具去替换就会遇到一些棘手的问题,如何识别哪些是函数,哪些是系统函数(系统函数不需要添加后缀)等。

随后想到的另一个解决方案是C++的方案,为其中一个版本库中的所有文件添加命名空间。然后使用g++将这部分代码编译成.o文件,之后再使用gcc将这些.o文件与整个程序中的其他代码进行链接。

不过需要注意的是,g++编译后所有导出接口名都会变化得不那么直观。


第三种方案完全解决了以上两种方案的痛点。

考虑一个C语言的编译链接过程。

首先会将每个c文件编译成.o文件。

在编译过程中,导出函数并不会被实际分配地址,而是将函数名以F符号的方式存在.o文件的符号表中。

在本c文件调用的函数如果不存在于本文件,也会生成一个UND的符号存在.o文件的符号表中。

在链接过程中,链接器接收输入的.o文件,为每个.o文件中的符号分存地址,并生成可执行文件。

有了这几点事实,问题就变得的简单多了。

首先将其中一个版本的库中所有代码编译为.o文件。然后收集所有.o文件中的F符号。

由于整个库代码有内部依赖关系,收集到的F符号必然是所有.o文件中UND符号的超集。

换句话说,所有的F符号名就是我们要重命名的所有函数名。

这里我们需要借助objdump和objcopy工具。objdump -t 用于列表.o文件的符号表,objcopy用于重命名符号。

我随手写了一段用于过虑F符号的lua脚本

--rename.lua
local list = {}
local reg = "([^%s]+)%s+([^%s]+)%s+([^%s]+)"..
        "%s+([^%s]+)%s+([^%s]+)%s+([^%s]+)"
for l in io.stdin:lines() do
	local a,b,c,d,e,f = string.match(l, reg)
	if a and c == "F" then
		list[#list + 1] = " --redefine-sym "
		list[#list + 1] = string.format("%s=%s_v2", f, f)
	end
end
print("#/bin/sh")
print("objcopy " .. table.concat(list) .. " $1")

我们可以使用如下命令来收集所有.o文件的F符号, 并产生修改符号所用的脚本

find . -name '*.o' | xargs objdump -t | ./lua rename > rename.sh

现在我们只需要再执行一条命令就可以把所有函数名增加一个_v2的后缀.

find . -name '*.o' | xargs -n 1 sh ./rename.sh

至此,我们这个版本的库代码的所有函数名已经全部增加了_v2后缀。

这些被处理过的.o文件与我们将所有.c代码中函数名重命名之后编译出的.o文件完全一等价。


8月2号补充:

在实际使用中发现, 局部函数(static 函数)符号有可能会被gcc做修饰,将被修饰的符号重命名会给我们带来一些麻烦,而我们原本也不需要去处理局部函数。

因此对rename.lua做如下修改,过虑掉非全局符号:

--rename.lua
local list = {}
local reg = "([^%s]+)%s+([^%s]+)%s+([^%s]+)"..
        "%s+([^%s]+)%s+([^%s]+)%s+([^%s]+)"
for l in io.stdin:lines() do
	local a,b,c,d,e,f = string.match(l, reg)
	if a and c == "F" and b == "g" then
		list[#list + 1] = " --redefine-sym "
		list[#list + 1] = string.format("%s=%s_v2", f, f)
	end
end
print("#/bin/sh")
print("objcopy " .. table.concat(list) .. " $1")

Git之坑

最近在使用Git进行多人协作时遇到了一些坑,在些处记录一下。

在使用git的过程中,最频繁使用的应该就是git pull了。在git pull命令时,一般会遇到下面几种情况。

如果是本地完全没有修改,则可以顺利进行git pull,如果有本地提交会与git pull下来的commit进行自动merge(当然自动merge也有可能失败,手动解决一下冲突即可)。

如果本地的修改与被git pull下来的commit中修改有相同的文件,这时git会报错提示,这时只让你先把本地修改commit或stash一下,也不会有太大问题。

最坑的就是是本地有修改,并且与git pull下来的commit并没有修改相同的文件。但是本地的commit(即没有push到远程仓库的提交)与git pull下来的commit自动合并时冲突了,这里git会提醒你要使用git commit -a来提交,但是根据git commit -a的语义,其实他不仅仅是把冲突内容全部合并,还会把本地的其他没有完成的修改一并提交上去。

一般来说,本地的修改大都是不完整的修改,这里如果提交上去,很容易造成其他问题。因此为了保险起见,如果本地有修改一定要先使用git stash将本地修改暂存之后,再执行git pull命令去拉取代码。

上面的坑,最多只会造成不完整的修改被提交,很容易及时发现。

但是另外一个关于submodule的坑,在超过一个人协作的情况下如果不注意是必然会发生的事。

假设有A和B两个人进行协作修改工程P。在P中引用了submodule S。

最初P-A(A的本地仓库)与P-B(B的本地仓库)中对submodule S的引用都是commit 1(SHA-1:xxxxxxxxxxx)。

这里S仓库有一个bug被修复了,P-A将自己的本地仓库中S的引用更新至commit 2(SHA-1:xxxxxxxxxxxxx)。然后提交并push到远程仓库。

这之后,B对P-B仓库成功执行了git pull命令。理论上P-B的仓库此时对S仓库的引用已经变更为commit 2了。但是由于S在P-B目录中的最新commit依然为commit 1。

在这之后,其实P-B中的S仓库并没有任何变动,依然为commit1,并且除了使用git status可以主动查看外,git 在合并之后并没有任何主动提交。这时P-B在完成某项修改之后,直接使用git commit -a就会重新将对S仓库的引用变更为commit 1。并且git 不会有任何提示。

如果要解决这个问题,必须要每次git pull之后,立即执行git submodule update –init来将仓库中的S目录更新到当前最新引用值。

为什么会有自动化测试框架

在研究敏捷开发中的TDD过程中, 接触到了UT(UnitTest)框架的概念.The Lego Batman Movie (2017)

在我的印象中, 自动化测试只需要每一个C模块中写一个对应的测试代码文件, 然后在Makefile稍加修改即可完成, 类似下面这样:

//module_a.h
int module_a_dosomething();
//module_a_test.c
int main()
{
module_a_dosomething();
}
//Makefile
....
test:module_a_test
./module_a_test
module_a_test:module_a_test.c module_a.c module_a.h
gcc -o $@ $^

只要略做扩展不难实现所有模块的自动化测试, 实在想不到UT框架有什么用.

本着存在就是有道理的原则, 研究了一下gtest. 发现我之前的理解并没有什么原则上的错误, 只不过UT框架提供了一些很方便的接口.

在上述不使用UT框架的方案中, 所有的代码测试打印均需要自己处理, 每一个模块的测试代码可能都含有相同的测试逻辑, 造成代码冗余.
将这些相同的测试逻辑抽出来进行整理, 一套UT的框架就可以产生了.


以google开源的UT框架gtest为例.

gtest中的ASSERT_*, EXPECT_*, TEST_F…等宏可以用来格式化输出test case的执行情况, 使得测试过程中结果更为清晰, 在编写test case时可以专心编写测试逻辑而几乎不需要关注测试打印的问题.

gtest将一些大部分test case代码中都会用到的测试逻辑提练出如事件、参数化、死亡测试, 运行参数等机制, 并导出接口供test case代码使用. 使得test case代码大大缩短, 测试逻辑可以更简单明了.

因此UT框架几乎是自动化测试的发展的必然产物, 当然到底是封装成Framework还是library, 这就要看作者的心意了.

vim写程序常用技巧


"Set ma pleader
let g:mapleader = ","

set nocompatible

set backspace=indent,eol,start

"显示行号
set nu

"标签
let g:miniBufExplMapWindowNavVim = 1
let g:miniBufExplMapWindowNavArrows = 1
let g:miniBufExplMapCTabSwitchBufs = 1
let g:miniBufExplModSelTarget = 1

"windows manger
let g:winManagerWindowLayout='FileExplorer'
nmap wm :WMToggle

"打开语法高亮
syntax on
"let asmsyntax="gas"
let asmsyntax="nasm"
"设置字体
set guifont=DejaVu Sans Mono 12

"设置缩进
set softtabstop=8
set shiftwidth=8
set expandtab

"关闭toolbar
set guioptions-=T

"关闭自动备份
set nobackup

set completeopt=longest,menu

"自动格式化
set formatoptions=tcrqn

"在行和段开始处使用制表符
set smarttab

"在normal模式下使用系统剪贴板
"set clipboard+=unnamed

"自动缩进设置
set cindent
set smartindent
set incsearch
set autoindent
set cinoptions=:0
"Show matching bracets
set showmatch

"Get out of VI's compatible mode
set nocompatible

"Have the mouse enabled all the time
set mouse=a

"Set to auto read when a file is changed from the outside
set autoread

"Enable filetype plugin
filetype plugin indent on

"设置配色方案为torte
"colo torte
colo desert
"colo tango
"设置支持的文件编码类项,目前设置为utf-8和gbk两种类型
set fenc=utf-8
set fileencodings=utf-8,chinese,gb18030,gbk,gb2312,cp936
set enc=utf-8
let &termencoding=&encoding

"设置断词
set linebreak

"设置搜索结果高亮显示
set hlsearch

"设置记录的历史操作列表
set history=200

"设置折叠
set foldenable
set foldcolumn=2
set foldlevel=3

"打开目录时不显示隐藏目录和文件
let g:netrw_hide= 1
let g:netrw_list_hide= '^..*'

"AutoCommand
" 鼠标跳到上次关闭时,编辑的位置
" When editing a file, always jump to the last known cursor position.
" Don't do it when the position is invalid or when inside an event handler
" (happens when dropping a file on gvim).
autocmd BufReadPost *
if line("'"") > 0 && line("'"") <= line("$") | exe "normal g`"" | endif "新建.c,.h.cpp,.sh,.java,.php,.py文件自动打开Taglist autocmd BufNewFile *.[ch],*.cpp,*.sh,*.java,*.php,*.py exec ":call SetTitle()" "读入.c,.h.cpp,.sh,.java,.php,.py文件自动打开Taglist "autocmd BufRead *.[ch],*.cpp,*.sh,*.java,*.php,*.py exec ":Tlist" "新建文件后,自动定位到文件末尾 autocmd BufNewFile * normal G "如果是新建的php文件,则自动定位到最后第二行 autocmd BufNewFile *.php normal k "写入.c,.h.cpp,.sh,.java,.php,.py文件自动更新ctags autocmd BufWrite *.[ch],*.cpp,*.sh,*.java,*.php,*.py exec ":!ctags -R *" " "读入python文件,设置缩进格式 autocmd BufNewFile,BufRead *.py set cinwords=if,elif,else,for,while,try,expect,finally,def,class "读入C文件,设置折叠方式为syntax autocmd BufNewFile,BufRead *.[ch],*.cpp set foldmethod=syntax "读入其它文件,设置折叠方式为indent autocmd BufNewFile,BufRead *.py,*.sh,*.java,*.php set foldmethod=indent "设置Java代码的自动补全 autocmd FileType java setlocal omnifunc=javacomplete#Complete "autocmd FileType java set tags=./tags,./../tags,./http://www.cnblogs.com/tags "设置输入代码的自动补全 "autocmd BufEnter * call DoWordComplete() set complete=.,w,b,u,t,i,k set completeopt=longest,menu "设置当回复邮件时自动定位到最后一行 autocmd BufRead /tmp/mutt-* normal G "autocmd BufRead /tmp/mutt-* normal $ "绑定自动补全的快捷键;
imap ;

"绑定复制到系统剪贴板快捷键
vmap c "+y
nmap c "+y

"绑定粘贴系统剪贴板内容快捷键
"imap v "+p "不设置insert模式下的快捷键,因为会造成无法输入,v
vmap v "+p
nmap v "+p

"设定开关Taglist插件的快捷键为F4,可以在VIM的左侧栏列出函数列表等
map :Tlist

"设置程序的运行和调试的快捷键F5和Ctrl-F5
map :call CompileRun()
map :call Debug()
"设置手动更新tags文件
map :!ctags -R *
map :!splint %
"设置tab操作的快捷键,绑定:tabnew到t,绑定:tabn, :tabp到n,
"p
map t :tabnew
map n :tabn
map p :tabp

"设置空格键开关折叠
nmap @=((foldclosed(line('.')) < 0) ? 'zc' : 'zo')

"使用r打开上次运行的命令
nmap r :

"用cscope支持
set csprg=/usr/bin/cscope
let Tlist_Ctags_Cmd='/usr/local/bin/ctags'
let Tlist_Show_One_File=1
let Tlist_Exit_OnlyWindow=1
let Tlist_Use_Right_Window=1
"默认打开Taglist
let Tlist_Auto_Open=1

"设置搜索的tags文件范围
set tags=./tags,./../tags,./http://www.cnblogs.com/tags,/usr/include/tags,/usr/src/linux-3.2.6/include/tags

"使用e打开当前文件同目录中的文件
if has("unix")
map e :e =expand("%:p:h") . "/"
else
map e :e =expand("%:p:h") . ""
endif

"定义CompileRun函数,用来调用进行编译和运行
func CompileRun()
exec "w"
"C程序
if &filetype == 'c'
exec "!gcc % -g -o %<" exec "!./%<" "Java程序 elseif &filetype == 'java' exec "!javac %" exec "!java %<" "php程序 elseif &filetype == 'php' exec "!php %" "bash程序 elseif &filetype == 'sh' exec "!bash %" "python程序 elseif &filetype == "python" exec "!python %" endif endfunc "结束定义CompileRun "定义Debug函数,用来调试程序 func Debug() exec "w" "C程序 if &filetype == 'c' exec "!gcc % -g -o %<" exec "!gdb %<" "Java程序 elseif &filetype == 'java' exec "!javac %" exec "!jdb %<" "Php程序 elseif &filetype == 'php' exec "!php %" "bash程序 elseif &filetype == 'sh' exec "!bash -x %" "python程序 elseif &filetype == 'python' exec "!pdb %" endif endfunc "结束定义Debug "定义函数SetTitle,自动插入文件头 func SetTitle() "如果文件类型为.sh文件 if &filetype == 'sh' || &filetype == 'python' call setline(1, "#========================================================================") call append(line("."), "# Author: findstr") call append(line(".")+1, "# Email: findstr@sina.com") call append(line(".")+2, "# File Name: ".expand("%")) call append(line(".")+3, "# Description: ") call append(line(".")+4, "# ") call append(line(".")+5, "# Edit History: ") call append(line(".")+6, "# ".strftime("%Y-%m-%d")." File created.") call append(line(".")+7, "#========================================================================") call append(line(".")+8, "") "其它程序文件 else call setline(1, "/**") call append(line("."), "=========================================================================") call append(line(".")+1, " Author: findstr") call append(line(".")+2, " Email: findstr@sina.com") call append(line(".")+3, " File Name: ".expand("%")) call append(line(".")+4, " Description: (C) ".strftime("%Y-%m"). " findstr") call append(line(".")+5, " ") call append(line(".")+6, " Edit History: ") call append(line(".")+7, " ".strftime("%Y-%m-%d")." File created.") call append(line(".")+8, "=========================================================================") call append(line(".")+9, "**/") call append(line(".")+10, "") endif "如果为php文件,添加相应头和尾 if &filetype == 'php' call append(0, "")
endif
"如果为sh文件,添加相应的头
if &filetype == 'sh'
call append(0, "#!/bin/bash")
"如果为python文件,添加相应的头和编码设定
elseif &filetype == 'python'
call append(0, "#!/usr/bin/python")
call append(1, "# -*- coding: utf-8 -*-")
endif
endfunc

"ececute project relate configuration in current directory
if filereadable("workspace.vim")
source workspace.vim
endif

关于ld -Ttext中的大小与text段在elf文件中的偏移之间的关系的猜想

在操作系统加载elf文件时都是按页映射的,而IA32下一页一般为4k,如果ld-Ttext 0x80400那么在操作系统去映设分页时应该是映射到0x80000~0x81000这个页表上的,但因为我们的text段入口地址为0x80400为了让程序去正确找到入口点,可以在text段之前构造SHT_NULL类型的无效段以便使text在文件中的偏移为0x400,这样在映射到页后,在加载时,直接跳到0x80400程序就能正常运行!