在我们开发app过程中一般认为main函数是程序执行的入口,但是在main函数之前系统已经做了很多的工作,今天我们就来研究一下。
现在已知load函数的处理是在main函数之前,我们在自定义类中实现load函数并打断点,通过bt命令查看堆栈信息,我们看到最先执行的是dyld库中的_dyld_start
dyld是苹果的动态链接器,在内核做好准备之后就将控制权交给dyld进行剩余的工作,他是开源的本文基于dyld-852
源码探索
_dyld_start
#if __arm64__ && !TARGET_OS_SIMULATOR
.text
.align 2
.globl __dyld_start
__dyld_start:
......
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
// 根据注释知道这里调用了dyldbootstrap::start
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
// 将dyldbootstrap::start的返回值存储到x16中
mov x16,x0
......
// 执行x16中返回的函数地址
br x16
......
#endif // __arm64__ && !TARGET_OS_SIMULATOR
通过分析可知这里调用了dyldbootstrap::start并执行返回值
dyldbootstrap::start
//
// This is code to bootstrap dyld. This work in normally done for a program by dyld and crt.
// In dyld we have to do this manually.
//
// 我们看到参数中包含
// appsMachHeader 就是应用程序的mach header
// dyldsMachHeader 就是dyld的mach header
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
// 对于系统来说dyld也是一个应用,在使用任何全局变量之前必须先做这一步
// 我们看到传入的参数是dyld的mach header
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
// 堆栈保护
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
// 这里应该是获取应用程序的的ASLR
uintptr_t appsSlide = appsMachHeader->getSlide();
// 返回dyld的_main函数,注意这里是dyld的_main函数
// appsMachHeader是Mach Header
// appsSlide是ASLR或者跟ASLR相关的量
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
这里主要涉及到三步
dyld地址重定位- 堆栈保护
- 返回
dyld的_main函数
rebaseDyld
//
// On disk, all pointers in dyld's DATA segment are chained together.
// They need to be fixed up to be real pointers to run.
// 在磁盘上所有访问DATA segment的指针都指向基地址0,这里需要根据偏移量slide进行校准,实际地址=slide+偏移量
static void rebaseDyld(const dyld3::MachOLoaded* dyldMH)
{
// walk all fixups chains and rebase dyld
const dyld3::MachOAnalyzer* ma = (dyld3::MachOAnalyzer*)dyldMH;
assert(ma->hasChainedFixups());
// ma就是dyld的mach header地址,也就是偏移的地址
uintptr_t slide = (long)ma; // all fixup chain based images have a base address of zero, so slide == load address
__block Diagnostics diag;
ma->withChainStarts(diag, 0, ^(const dyld_chained_starts_in_image* starts) {
ma->fixupAllChainedFixups(diag, starts, slide, dyld3::Array<const void*>(), nullptr);
});
diag.assertNoError();
// now that rebasing done, initialize mach/syscall layer
mach_init();
// <rdar://47805386> mark __DATA_CONST segment in dyld as read-only (once fixups are done)
ma->forEachSegment(^(const dyld3::MachOFile::SegmentInfo& info, bool& stop) {
if ( info.readOnlyData ) {
::mprotect(((uint8_t*)(dyldMH))+info.vmAddr, (size_t)info.vmSize, VM_PROT_READ);
}
});
}
dyld::_main
这个函数很长,足有一千多行,我们挑一些主要代码分析
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
// 通过注释可以知道这个函数返回程序的main()函数地址
// 所以我们从return result位置反推
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
// 1、配置上下文环境和检查上下文环境
setContext(mainExecutableMH, argc, argv, envp, apple);
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
// 通过配置 DYLD_PRINT_ENV 可以打印环境变量
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
// 2、加载共享缓库
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
mapSharedCache(mainExecutableSlide);
// 3、实例化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
// 4、加载插入的动态库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// 5、链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
// 6、链接插入的动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
...
}
// 7、递归绑定主程序以及主程序所依赖的库
// Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);
// 8、递归绑定插入的镜像文件
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}
// 9、弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
// 10、执行所有的初始化操作
// run all initializers
initializeMainExecutable();
// 11、返回应用程序的main函数
return result;
}
这个函数很长,是本次研究的核心方法,我们很难从头到尾进行分析,于是根据函数返回值return result;进行反推得知result是由sMainExecutable进行赋值的,于是我们就围绕sMainExecutable进行重点分析
这里是dyld进行应用程序加载的主要流程:
- 1、配置上下文环境和检查上下文环境
- 2、加载共享缓库
- 3、实例化主程序
- 4、加载插入的动态库
- 5、链接主程序
- 6、链接插入的动态库
- 7、递归绑定主程序以及主程序所依赖的库
- 8、递归绑定插入的镜像文件
- 9、弱符号绑定
- 10、执行所有的初始化操作
通过上文可知函数调用顺序为_dyld_start->start->_main,最终_dyld_start拿到的是_main的返回值,也就是应用程序的main函数并执行
1、配置上下文环境和检查上下文环境
这里我们知道了通过配置 DYLD_PRINT_ENV 可以打印环境变量
运行之后可以看到好多环境变量
例如这里可以看到插入了三个动态库分别为
libBacktraceRecording.dyliblibMainThreadChecker.dyliblibViewDebuggerSupport.dylib
2、加载共享缓库
static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if TARGET_OS_OSX
// OSX的逻辑不用看
#if __i386__
// i386的逻辑不看
#endif
#endif
#if TARGET_OS_SIMULATOR
// 模拟器的逻辑不用看
#endif
// iOS cannot run without shared region
// iOS不能脱离共享缓存库运行
}
虽然这个方法中没有arm64架构的代码,但是最后一句备注说的很清楚,iOS不能脱离共享缓存库运行,这足以说明其重要性
static void mapSharedCache(uintptr_t mainExecutableSlide)
{
......
loadDyldCache(opts, &sSharedCacheLoadInfo);
......
}
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
#if TARGET_OS_SIMULATOR
// simulator only supports mmap()ing cache privately into process
return mapCachePrivate(options, results);
#else
// 如果为强制私有类型那么只加载到当前进程
if ( options.forcePrivate ) {
// mmap cache into this process only
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
// 如果已经存在于共享缓存库中,那就复用
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
} else {
// slow path: this is first process to load cache
// 如果第一次使用该库,那么就加载到共享缓存库
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}
共享缓存库可以在多个进程间共享,节省内存空间
3、实例化主程序
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
// 在dyld获得控制之前,内核映射到主可执行文件中。
// 我们需要为主可执行文件中已经映射的对象创建一个ImageLoader*。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
// if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// 将MachO中image添加到imageList中
// 就是我们通过控制台命令image list查看到的信息
addImage(image);
return (ImageLoaderMachO*)image;
// }
// throw "main executable not a known format";
}
传入了三个参数
- 1、应用程序的
mach header指针 - 2、偏移量
slide - 3、路径
path有了这些信息loader就可以进行应用程序MachO文件的的加载。这里涉及到了MachO相关操作,就是根据Mach header等信息从MachO中读取内容的过程,这个我们后面重点研究MachO文件的时候再详细介绍
4、加载插入的动态库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
DYLD_INSERT_LIBRARIES我们在第一步配置环境变量的时候看到了他,就是将用到的动态库加载进来
5、链接主程序
void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
{
// add to list of known images. This did not happen at creation time for bundles
// 如果开发的是一个bundle
if ( image->isBundle() && !image->isLinked() )
addImage(image);
// we detect root images as those not linked in yet
// 检测到根图还没有链接到,也就是我们的主程序还没有链接到
if ( !image->isLinked() )
addRootImage(image);
// process images
try {
const char* path = image->getPath();
#if SUPPORT_ACCELERATE_TABLES
if ( image == sAllCacheImagesProxy )
path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
#endif
// 主程序链接到之后再去链接MachO中的其他信息
image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
}
catch (const char* msg) {
garbageCollectImages();
throw;
}
}
链接完根镜像再去链接其他内容
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
......
// 链接的文件还可能需要依赖其他文件,这个链接过程是递归的
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
......
}
6、链接插入的动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// 链接完主程序再来执行
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
// 以确保插入动态库所依赖的动态库不会在程序使用的动态库之前
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
...
}
7、递归绑定主程序以及主程序所依赖的库
// Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
// 递归绑定主程序以及主程序所依赖的其他库文件
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);
8、递归绑定插入的镜像文件
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 这个过程也是递归的
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}
9、弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
10、执行所有的初始化操作
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
// 先执行所有插入库的初始化工作
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
// 再执行主程序以及主程序所依赖文件的初始化
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
这个函数重点在于初始化,在经过runInitializers->processInitializers->recursiveInitialization调用之后
看一下recursiveInitialization
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
// initialize this image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
// 初始化完成以后发通知
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
}
看一下notifySingle
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
......
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
......
}
赋值操作是在
// _dyld_objc_notify_init
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
......
// record functions to call
sNotifyObjCInit = init;
......
}
被_dyld_objc_notify_register调起
// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
在dyld源码中并没有搜索到_dyld_objc_notify_register的调起位置,通过符号断点查看是在objc4库中的_objc_init中
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
// 在这里吊起了
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
小结
我们先简单介绍一下库、链接、绑定这几个概念。
库简单来说就是一段编译好的二进制代码,加上头文件就可以给别人用,根据链接方式可分为静态库和动态库。
静态库在编译的时候会被直接拷贝一份,复制到目标程序里,可以理解为一个开发阶段的东西,打包ipa之后就成了应用程序的一部分。
动态库在编译时并不会被拷贝到目标程序中,只是预留了指向动态库的路径,在运行阶段才会绑定真正的动态库地址,多个程序可以共享同一份动态库。
动态库分为私有动态库和非私有动态库(不确定这样说准确不准确😄),非私有动态库就是系统提供的这些动态库,可以被程序共享,但是苹果不允许用户开发这种动态库,苹果只允许我们开发使用私有动态库,即只有当前程序自己使用的动态库,私有动态库不会被加载到共享缓存库中。
Framework是一种打包方式,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发,Framework是动态库还是静态库是由所包含的二进制库文件决定的。
链接和绑定主要过程包括了地址和空间分配、符号解析和重定位。链接器将经过汇编器处理的所有目标文件和库进行链接形成最终的可执行文件。总而言之,链接的主要内容就是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地衔接(符号解析,即 符号决议 / 地址绑定,符号决议倾向于静态链接,地址绑定倾向于动态链接,即适用范围的区别)
我们简单理解为经过链接和绑定之后程序的各个模块之间就已经衔接好了,成了一个完整的可执行程序,可以被运行起来了。
这里基本上都是在对image list做操作,我们看一下image list中的image究竟是个啥,字面意思是镜像文件,也就是将磁盘上的代码拷贝到内存中去,内存中的代码就是磁盘代码的镜像文件,一毛一样的,就好像镜子一样。
在项目中的控制台通过image list命令打印发现
// 第0个是当前工程的可执行文件
[ 0] xxx xxx xxx/xxx/JZGObjcBuild
// 紧接着是dyld、libc++.1.dylib、libSystem
[ 1] xxx xxx /usr/lib/dyld
[ 2] xxx xxx /usr/lib/libc++.1.dylib
[ 3] xxx xxx /usr/lib/libSystem.B.dylib
// 后面都是动态库或者framework(也就是动态库+头文件+资源文件)
[ x] /xxx.framework/x/x/xx.dylib
[ x] /xxx.framework/x/x/可执行文件
......
image list包含当前程序的可执行文件和系统提供的动态库。当我们运行多个应用程序的时候可以从共享缓存复用动态库文件,达到节省内存空间的效果。
dyld加载应用程序的几个大的步骤
- 1、配置上下文环境和检查上下文环境
- 2、加载共享缓库
- 3、实例化主程序
- 4、加载插入的动态库
- 5、链接主程序
- 6、链接插入的动态库
- 7、递归绑定主程序以及主程序所依赖的库
- 8、递归绑定插入的镜像文件
- 9、弱符号绑定
- 10、执行所有的初始化操作
- 11、返回应用程序的
main函数
那么我们可以简单将这些步骤进行分类:
- 1、配置环境
- 2-9、解决主程序各个部分之间的衔接问题,使主程序变得完整和可执行
- 10-11、执行主程序的初始化工作、将主程序跑起来并执行
main函数
dyld2 & dyld3
在wwdc2017中介绍了dyld3以及相对于dyld2的不同点
在iOS 13之前使用的是dyld2,主要流程为
- 解析
Mach-O文件,递归找到依赖库 - 加载
Mach-O文件 - 符号查找
- 绑定
- 初始化程序并运行
这里有大量的计算操作和I/O操作,为了提升App启动速度引入了dyld3
dyld3被分为了三个组件
- 本进程外的
Mach-O分析器/编译器- 预先处理了所有可能影响启动速度的 search path、@rpaths 和环境变量
- 然后分析 Mach-O 的 Header 和依赖,并完成了所有符号查找的工作
- 最后将这些结果创建成了一个启动闭包
- 这是一个普通的 daemon 进程,可以使用通常的测试架构
- 本进程内执行
launch closure引擎- 验证启动闭包的安全性,然后映射到 dylib 之中,再跳转到 main 函数
- 不再需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。
- 一个启动闭包缓存服务
- 系统 App 的启动闭包被构建在一个 Shared Cache 中, 我们甚至不需要打开一个单独的文件
- 对于第三方的 App,我们会在 App 安装或者升级的时候构建这个启动闭包。
- 在 iOS、tvOS、watchOS中,这一切都是 App 启动之前完成的。在 macOS 上,由于有 Side Load App,进程内引擎会在首次启动的时候启动一个 daemon 进程,之后就可以使用启动闭包启动了。
我们在上文中看的是dyld2的流程
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
dyld3的流程在
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
if ( gUseDyld3 )
return dyld3::_dyld_objc_notify_register(mapped, init, unmapped);
}
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
gAllImages.setObjCNotifiers(mapped, init, unmapped);
}
void AllImages::setObjCNotifiers(_dyld_objc_notify_mapped map, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmap)
{
......
// 在这里分析
}