ART加载OAT文件的分析
本文对老罗博客http://blog.****.net/luoshengyang/article/details/39307813 进行学习理解,针对android6.0系统源码,连个人理解带复制粘贴,总结的ART虚拟机对OAT文件的加载解析流程。
声明:由于****官方bug,文章编码全部整合为乱码,无法显示代码,所以重新整合,代码用普通字体排版。
ART运行时提供了一个OatFile类,通过调用它的静态成员函数Open可以在本进程中加载OAT文件,它的实现如下所示(art/runtime/oat_file.cc):
OatFile* OatFile::Open(conststd::string& filename,
const std::string&location,
uint8_t* requested_base,
uint8_t* oat_file_begin,
bool executable,
const char*abs_dex_location,
std::string* error_msg){
CHECK(!filename.empty()) << location;
CheckLocation(location);
std::unique_ptr<OatFile> ret;
//Use dlopen only when flagged to do so, and when it's OK to load thingsexecutable.
//TODO: Also try when not executable? The issue here could be re-mapping aswritable (as
// !executable is a signthat we may want to patch), which may not be allowed for
// various reasons.
if(kUseDlopen && (kIsTargetBuild || kUseDlopenOnHost) &&executable) {
// Try to use dlopen. This may fail for various reasons, outlined below.We try dlopen, as
// this will register the oat file with the linker and allows libunwindto find our info.
ret.reset(OpenDlopen(filename, location, requested_base,abs_dex_location, error_msg));
if (ret.get() != nullptr) {
return ret.release();
}
if (kPrintDlOpenErrorMessage) {
LOG(ERROR) << "Failed to dlopen: " << *error_msg;
}
}
//If we aren't trying to execute, we just use our own ElfFile loader for a couplereasons:
//
//On target, dlopen may fail when compiling due to selinux restrictions oninstalld.
//
//We use our own ELF loader for Quick to deal with legacy apps that
//open a generated dex file by name, remove the file, then open
//another generated dex file with the same name. http://b/10614658
//
//On host, dlopen is expected to fail when cross compiling, so fall back toOpenElfFile.
//
//
//Another independent reason is the absolute placement of boot.oat. dlopen on thehost usually
//does honor the virtual address encoded in the ELF file only for ET_EXEC files,not ET_DYN.
std::unique_ptr<File>file(OS::OpenFileForReading(filename.c_str()));
if(file == nullptr) {
*error_msg = StringPrintf("Failed to open oat filename for reading:%s", strerror(errno));
return nullptr;
}
ret.reset(OpenElfFile(file.get(),location, requested_base, oat_file_begin, false, executable,
abs_dex_location,error_msg));
//It would be nice to unlink here. But we might have opened the file created bythe
//ScopedLock, which we better not delete to avoid races. TODO: Investigate how tofix the API
//to allow removal when we know the ELF must be borked.
return ret.release();
}
参数filename和location实际上是一样的,指向要加载的OAT文件。参数requested_base是一个可选参数,用来描述要加载的OAT文件里面的oatdata段要加载在的位置。参数executable表示要加载的OAT是不是应用程序的主执行文件。
上述代码简言之就是根据ART_USE_PORTABLE_COMPILER宏选择用OpenDlopen还是OpenElfFile方法加载OAT文件。接下来分析这两个函数。
首先分析OpenDlopen函数(art/runtime/oat_file.cc):
OatFile* OatFile::OpenDlopen(conststd::string& elf_filename,
conststd::string& location,
uint8_t*requested_base,
const char*abs_dex_location,
std::string* error_msg) {
std::unique_ptr<OatFile> oat_file(new OatFile(location, true));
bool success = oat_file->Dlopen(elf_filename, requested_base,abs_dex_location, error_msg);
if(!success) {
return nullptr;
}
return oat_file.release();
}
跳转到Dlopen函数(一下代码为极度精简版,源代码请自行查阅art/runtime/oat_file.cc):
bool OatFile::Dlopen(const std::string&elf_filename, byte* requested_base) {
char* absolute_path = realpath(elf_filename.c_str(), NULL);
......
dlopen_handle_ = dlopen(absolute_path, RTLD_NOW);
......
begin_ = reinterpret_cast<byte*>(dlsym(dlopen_handle_,"oatdata"));
......
if(requested_base != NULL && begin_ != requested_base) {
......
return false;
}
end_ = reinterpret_cast<byte*>(dlsym(dlopen_handle_,"oatlastword"));
......
//Readjust to be non-inclusive upper bound.
end_ += sizeof(uint32_t);
return Setup();
}
Dlopen首先通过动态链接器提供的dlopen函数将参数elf_filename指定的OAT文件加载到内存中来,接着同样是通过动态链接器提供的dlsym函数从加载的OAT文件获得oatdata和oatlastword的地址,分别保存在当前正在处理的OatFile对象的成员变量begin_和end_中。oatdata的地址即为OAT文件里面的oatdata段加载到内存中的开始地址,而oatlastword的地址即为OAT文件里面的oatexec加载到内存中的结束地址。符号oatlastword本身也是属于oatexec段的,它自己占用了一个地址,也就是sizeof(uint32_t)个字节,于是将前面得到的end_值加上sizeof(uint32_t),得到的才是oatexec段的结束地址。
实际上,上面得到的begin_值指向的是加载内存中的oatdata段的头部,即OAT头。这个OAT头描述了OAT文件所包含的DEX文件的信息,以及定义在这些DEX文件里面的类方法所对应的本地机器指令在内存的位置。另外,上面得到的end_是用来在解析OAT头时验证数据的正确性的。此外,如果参数requested_base的值不等于0,那么就要求oatdata段必须要加载到requested_base指定的位置去,也就是上面得到的begin_值与requested_base值相等,否则的话就会出错返回。
最后,OatFile类的成员函数Dlopen通过调用另外一个成员函数Setup来解析已经加载内存中的oatdata段,以获得ART运行时所需要的更多信息。我们分析完成OatFile类的静态成员函数OpenElfFile之后,再来看OatFile类的成员函数Setup的实现。
接着分析OpenElfFile函数的实现,就是另外一种加载OAT文件的函数(art/runtime/oat_file.cc):
OatFile* OatFile::OpenElfFile(File* file,
conststd::string& location,
uint8_t*requested_base,
uint8_t*oat_file_begin,
bool writable,
bool executable,
const char*abs_dex_location,
std::string*error_msg) {
std::unique_ptr<OatFile> oat_file(new OatFile(location,executable));
bool success = oat_file->ElfFileOpen(file, requested_base,oat_file_begin, writable, executable,
abs_dex_location, error_msg);
if(!success) {
CHECK(!error_msg->empty());
return nullptr;
}
return oat_file.release();
}
跳转到ElfFileOpen函数:
bool OatFile::ElfFileOpen(File* file,uint8_t* requested_base, uint8_t* oat_file_begin,
bool writable, boolexecutable,
const char*abs_dex_location,
std::string*error_msg) {
//TODO: rename requested_base to oat_data_begin
elf_file_.reset(ElfFile::Open(file, writable,/*program_header_only*/true, error_msg,
oat_file_begin));
......
boolloaded = elf_file_->Load(executable, error_msg);
......
begin_ =elf_file_->FindDynamicSymbolAddress("oatdata");
......
end_= elf_file_->FindDynamicSymbolAddress("oatlastword");
......
//Readjust to be non-inclusive upper bound.
end_ += sizeof(uint32_t);
......
returnSetup(abs_dex_location, error_msg);}
bool OatFile::ElfFileOpen(File* file,uint8_t* requested_base, uint8_t* oat_file_begin,
bool writable, boolexecutable,
const char*abs_dex_location,
std::string*error_msg) {
//TODO: rename requested_base to oat_data_begin
elf_file_.reset(ElfFile::Open(file, writable,/*program_header_only*/true, error_msg,
oat_file_begin));
......
boolloaded = elf_file_->Load(executable, error_msg);
......
begin_ =elf_file_->FindDynamicSymbolAddress("oatdata");
......
end_= elf_file_->FindDynamicSymbolAddress("oatlastword");
......
//Readjust to be non-inclusive upper bound.
end_ += sizeof(uint32_t);
......
return Setup(abs_dex_location, error_msg);}
bool OatFile::ElfFileOpen(File* file,uint8_t* requested_base, uint8_t* oat_file_begin,
bool writable, boolexecutable,
const char* abs_dex_location,
std::string*error_msg) {
//TODO: rename requested_base to oat_data_begin
elf_file_.reset(ElfFile::Open(file, writable,/*program_header_only*/true, error_msg,
oat_file_begin));
......
boolloaded = elf_file_->Load(executable, error_msg);
......
begin_ =elf_file_->FindDynamicSymbolAddress("oatdata");
......
end_= elf_file_->FindDynamicSymbolAddress("oatlastword");
......
//Readjust to be non-inclusive upper bound.
end_ += sizeof(uint32_t);
......
return Setup(abs_dex_location, error_msg);}
OatFile类的静态成员函数OpenElfFile的实现与前面分析的成员函数Dlopen是很类似的,唯一不同的是前者通过ElfFile类来手动加载参数file指定的OAT文件,实际上就是按照ELF文件格式来解析参数file指定的OAT文件,并且将文件里面的oatdata段和oatexec段加载到内存中来。
接下来分析Dlopen和OpenElfFile流程上都要调用的Setup(abs_dex_location,error_msg)函数的实现(art/runtime/oat_file.cc),函数太长分为三段分析,首先看第一段:
bool OatFile::Setup(const char*abs_dex_location, std::string* error_msg) {
if(!GetOatHeader().IsValid()) {
std::string cause = GetOatHeader().GetValidationErrorMessage();
*error_msg = StringPrintf("Invalid oat header for '%s': %s",GetLocation().c_str(),
cause.c_str());
return false;
}
const uint8_t* oat = Begin();
oat+= sizeof(OatHeader);
if(oat > End()) {
*error_msg = StringPrintf("In oat file '%s' found truncatedOatHeader", GetLocation().c_str());
return false;
}
oat+= GetOatHeader().GetKeyValueStoreSize();
if(oat > End()) {
*error_msg = StringPrintf("In oat file '%s' found truncatedvariable-size data: "
"%p + %zd +%ud <= %p", GetLocation().c_str(),
Begin(),sizeof(OatHeader), GetOatHeader().GetKeyValueStoreSize(),
End());
return false;
}
其中,GetOatHeader、Begin和End函数的实现(art/runtime/oat_file.cc),如下所示:
const OatHeader&OatFile::GetOatHeader() const {
return *reinterpret_cast<const OatHeader*>(Begin());
}
const uint8_t* OatFile::Begin() const {
CHECK(begin_ != nullptr);
return begin_;
}
const uint8_t* OatFile::End() const {
CHECK(end_ != nullptr);
return end_;
}
这三个函数主要是涉及到了OatFile类的两个成员变量begin_和end_,由上边对两种加载OAT文件方法的分析,它们分别是OAT文件里面的oatdata段开始地址和oatexec段的结束地址。
通过OatFile类的成员函数GetOatHeader可以清楚地看到,OAT文件里面的oatdata段的开始储存着一个OAT头(art/runtime/oat.h):
class PACKED(4) OatHeader {
public:
......
private:
uint8_t magic_[4];
uint8_t version_[4];
uint32_t adler32_checksum_;
InstructionSet instruction_set_;
uint32_t dex_file_count_;
uint32_t executable_offset_;
uint32_t interpreter_to_interpreter_bridge_offset_;
uint32_t interpreter_to_compiled_code_bridge_offset_;
uint32_t jni_dlsym_lookup_offset_;
uint32_t portable_resolution_trampoline_offset_;
uint32_t portable_to_interpreter_bridge_offset_;
uint32_t quick_resolution_trampoline_offset_;
uint32_t quick_to_interpreter_bridge_offset_;
uint32_t image_file_location_oat_checksum_;
uint32_t image_file_location_oat_data_begin_;
uint32_t image_file_location_size_;
uint8_t image_file_location_data_[0]; // note variable width data at end
......
};
类OatHeader的各个成员变量的含义如下所示:
magic: 标志OAT文件的一个魔数,等于‘oat\n’。
version: OAT文件版本号,目前的值等于‘007、0’。
adler32_checksum_: OAT头部检验和。
instruction_set_: 本地机指令集,有四种取值,分别为 kArm(1)、kThumb2(2)、kX86(3)和kMips(4)。
dex_file_count_: OAT文件包含的DEX文件个数。
executable_offset_: oatexec段开始位置与oatdata段开始位置的偏移值。
interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_:ART运行时在启动的时候,可以通过-Xint选项指定所有类的方法都是解释执行的,这与传统的虚拟机使用解释器来执行类方法差不多。同时,有些类方法可能没有被翻译成本地机器指令,这时候也要求对它们进行解释执行。这意味着解释执行的类方法在执行的过程中,可能会调用到另外一个也是解释执行的类方法,也可能调用到另外一个按本地机器指令执行的类方法中。OAT文件在内部提供有两段trampoline代码,分别用来从解释器调用另外一个也是通过解释器来执行的类方法和从解释器调用另外一个按照本地机器执行的类方法。这两段trampoline代码的偏移位置就保存在成员变量 interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_。
jni_dlsym_lookup_offset_: 类方法在执行的过程中,如果要调用另外一个方法是一个JNI函数,那么就要通过存在放置jni_dlsym_lookup_offset_的一段trampoline代码来调用。
portable_resolution_trampoline_offset_和quick_resolution_trampoline_offset_: 用来在运行时解析还未链接的类方法的两段trampoline代码。其中,portable_resolution_trampoline_offset_指向的trampoline代码用于Portable类型的Backend生成的本地机器指令,而quick_resolution_trampoline_offset_用于Quick类型的Backend生成的本地机器指令。
portable_to_interpreter_bridge_offset_和quick_to_interpreter_bridge_offset_: 与interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_的作用刚好相反,用来在按照本地机器指令执行的类方法中调用解释执行的类方法的两段trampoline代码。其中,portable_to_interpreter_bridge_offset_用于Portable类型的Backend生成的本地机器指令,而quick_to_interpreter_bridge_offset_用于Quick类型的Backend生成的本地机器指令。
由于每一个应用程序都会依赖于boot.art文件,因此为了节省由打包在应用程序里面的classes.dex生成的OAT文件的体积,上述interpreter_to_interpreter_bridge_offset_、interpreter_to_compiled_code_bridge_offset_、jni_dlsym_lookup_offset_、portable_resolution_trampoline_offset_、portable_to_interpreter_bridge_offset_、quick_resolution_trampoline_offset_和quick_to_interpreter_bridge_offset_七个成员变量指向的trampoline代码段只存在于boot.art文件中。换句话说,在由打包在应用程序里面的classes.dex生成的OAT文件的oatdata段头部中,上述七个成员变量的值均等于0。
image_file_location_data_: 用来创建Image空间的文件的路径的在内存中的地址。
image_file_location_size_: 用来创建Image空间的文件的路径的大小。
image_file_location_oat_data_begin_: 用来创建Image空间的OAT文件的oatdata段在内存的位置。
image_file_location_oat_checksum_: 用来创建Image空间的OAT文件的检验和。
上述四个成员变量记录了一个OAT文件所依赖的用来创建Image空间文件以及创建这个Image空间文件所使用的OAT文件的相关信息。
通过OatFile类的成员函数Setup的第一部分代码的分析,我们就知道了,OAT文件的oatdata段在最开始保存着一个OAT头,如图所示:
我们接着再看OatFile类的成员函数Setup的第二部分代码:
oat+= GetOatHeader().GetKeyValueStoreSize();
if(oat > End()) {
*error_msg = StringPrintf("In oat file '%s' found truncatedvariable-size data: "
"%p + %zd +%ud <= %p", GetLocation().c_str(),
Begin(),sizeof(OatHeader), GetOatHeader().GetKeyValueStoreSize(),
End());
return false;
}
调用OatFile类的成员函数GetOatHeader获得的是正在打开的OAT文件的头部OatHeader,通过调用它的成员函数GetImageFileLocationSize获得的是正在打开的OAT依赖的Image空间文件的路径大小。变量oat最开始的时候指向oatdata段的开始位置。读出OAT头之后,变量oat就跳过了OAT头。由于正在打开的OAT文件引用的Image空间文件路径保存在紧接着OAT头的地方。因此,将Image空间文件的路径大小增加到变量oat去后,就相当于是跳过了保存Image空间文件路径的位置。
通过OatFile类的成员函数Setup的第二部分代码的分析,我们就知道了,紧接着在OAT头后面的是Image空间文件路径,如图所示:
我们接着再看OatFile类的成员函数Setup的第三部分代码:
for(size_t i = 0; i < GetOatHeader().GetDexFileCount(); i++) {
size_t dex_file_location_size = *reinterpret_cast<constuint32_t*>(oat);
......
oat += sizeof(dex_file_location_size);
......
const char* dex_file_location_data = reinterpret_cast<constchar*>(oat);
oat += dex_file_location_size;
......
std::string dex_file_location(dex_file_location_data,dex_file_location_size);
uint32_t dex_file_checksum = *reinterpret_cast<constuint32_t*>(oat);
oat += sizeof(dex_file_checksum);
......
uint32_t dex_file_offset = *reinterpret_cast<constuint32_t*>(oat);
......
oat += sizeof(dex_file_offset);
......
const uint8_t* dex_file_pointer = Begin() + dex_file_offset;
if (!DexFile::IsMagicValid(dex_file_pointer)) {
......
return false;
}
if (!DexFile::IsVersionValid(dex_file_pointer)) {
......
return false;
}
const DexFile::Header* header = reinterpret_cast<constDexFile::Header*>(dex_file_pointer);
const uint32_t* methods_offsets_pointer = reinterpret_cast<constuint32_t*>(oat);
oat += (sizeof(*methods_offsets_pointer) *header->class_defs_size_);
......
oat_dex_files_.Put(dex_file_location, new OatDexFile(this,
dex_file_location,
dex_file_checksum,
dex_file_pointer,
methods_offsets_pointer));
}
return true;
}
这部分代码用来获得包含在oatdata段的DEX文件描述信息。每一个DEX文件记录在oatdata段的描述信息包括:
1. DEX文件路径大小,保存在变量dex_file_location_size中;
2. DEX文件路径,保存在变量dex_file_location_data中;
3. DEX文件检验和,保存在变量dex_file_checksum中;
4. DEX文件内容在oatdata段的偏移,保存在变量dex_file_offset中;
5. DEX文件包含的类的本地机器指令信息偏移数组,保存在变量methods_offsets_pointer中;
在上述五个信息中,最重要的就是第4个和第5个信息了。
通过第4个信息,我们可以在oatdata段中找到对应的DEX文件的内容。DEX文件最开始部分是一个DEX文件头,上述代码通过检查DEX文件头的魔数和版本号来确保变量dex_file_offset指向的位置确实是一个DEX文件。
通过第5个信息我们可以找到DEX文件里面的每一个类方法对应的本地机器指令。这个数组的大小等于header->class_defs_size_,即DEX文件里面的每一个类在数组中都对应有一个偏移值。这里的header指向的是DEX文件头,它的class_defs_size_描述了DEX文件包含的类的个数。在DEX文件中,每一个类都是有一个从0开始的编号,该编号就是用来索引到上述数组的,从而获得对应的类所有方法的本地机器指令信息。
最后,上述得到的每一个DEX文件的信息都被封装在一个OatDexFile对象中,以便以后可以直接访问。如果我们使用OatDexFile来描述每一个DEX文件的描述信息,那么就可以通过图看到这些描述信息在oatdata段的位置:
为了进一步理解包含在oatdata段的DEX文件描述信息,我们继续看OatDexFile类的构造函数的实现,如下所示:
OatFile::OatDexFile::OatDexFile(const OatFile* oat_file,
const std::string& dex_file_location,
const std::string& canonical_dex_file_location,
uint32_t dex_file_location_checksum,
const uint8_t* dex_file_pointer,
const uint32_t* oat_class_offsets_pointer)
: oat_file_(oat_file),
dex_file_location_(dex_file_location),
canonical_dex_file_location_(canonical_dex_file_location),
dex_file_location_checksum_(dex_file_location_checksum),
dex_file_pointer_(dex_file_pointer),
oat_class_offsets_pointer_(oat_class_offsets_pointer) {}
OatDexFile类它将DEX文件描述息保存在相应的成员变量中。通过这些信息,我们就可以获得包含在该DEX文件里面的类的所有方法的本地机器指令信息。
例如,通过调用OatDexFile类的成员函数GetOatClass可以获得指定类的所有方法的本地机器指令信息:
- const OatFile::OatClass* OatFile::OatDexFile::GetOatClass(uint16_t class_def_index) const {
- uint32_t oat_class_offset = oat_class_offsets_pointer_[class_def_index];
- const byte* oat_class_pointer = oat_file_->Begin() + oat_class_offset;
- CHECK_LT(oat_class_pointer, oat_file_->End()) << oat_file_->GetLocation();
- mirror::Class::Status status = *reinterpret_cast<const mirror::Class::Status*>(oat_class_pointer);
- const byte* methods_pointer = oat_class_pointer + sizeof(status);
- CHECK_LT(methods_pointer, oat_file_->End()) << oat_file_->GetLocation();
- return new OatClass(oat_file_,
- status,
- reinterpret_cast<const OatMethodOffsets*>(methods_pointer));
- }
参数class_def_index表示要查找的目标类的编号。这个编号用作数组oat_class_offsets_pointer_(即前面描述的methods_offsets_pointer数组)的索引,就可以得到一个偏移位置oat_class_offset。这个偏移位置是相对于OAT文件的oatdata段的,因此将该偏移值加上OAT文件的oatdata段的开始位置后,就可以得到目标类的所有方法的本地机器指令信息。这些信息的布局如图5所示:
在OAT文件中,每一个DEX文件包含的每一个类的描述信息都通过一个OatClass对象来描述。为了方便描述,我们称之为OAT类。我们通过OatClass类的构造函数来理解它的作用,如下所示:
- OatFile::OatClass::OatClass(const OatFile* oat_file,
- mirror::Class::Status status,
- const OatMethodOffsets* methods_pointer)
- : oat_file_(oat_file), status_(status), methods_pointer_(methods_pointer) {}
参数oat_file描述的是宿主OAT文件,参数status描述的是OAT类状态,参数methods_pointer是一个数组,描述的是OAT类的各个方法的信息,它们被分别保存在OatClass类的相应成员变量中。通过这些信息,我们就可以获得包含在该DEX文件里面的类的所有方法的本地机器指令信息。
例如,通过调用OatClass类的成员函数GetOatMethod可以获得指定类方法的本地机器指令信息(注意代码与老罗博客区别):
const OatFile::OatMethod OatFile::OatClass::GetOatMethod(uint32_t method_index) const {const OatMethodOffsets* oat_method_offsets = GetOatMethodOffsets(method_index);
if (oat_method_offsets == nullptr) {
return OatMethod(nullptr, 0);
}
if (oat_file_->IsExecutable() ||
Runtime::Current() == nullptr || // This case applies for oatdump.
Runtime::Current()->IsAotCompiler()) {
return OatMethod(oat_file_->Begin(), oat_method_offsets->code_offset_);
}
// We aren't allowed to use the compiled code. We just force it down the interpreted / jit
// version.
return OatMethod(oat_file_->Begin(), 0);
}
参数method_index描述的目标方法在类中的编号,用这个编号作为索引,就可以在OatClass类的成员变量methods_pointer_指向的一个数组中找到目标方法的本地机器指令信息。这些本地机器指令信息封装在一个OatMethod对象,它们在OAT文件的布局如图6下所示:
为了进一步理解OatMethod的作用,我们继续看它的构造函数的实现,如下所示(art/runtime/oat_file.h):
OatMethod(const uint8_t* base, const uint32_t code_offset)
: begin_(base), code_offset_(code_offset) {
}
参数base描述的是OAT文件的OAT头在内存的位置,而参数code_offset描述的是类方法的本地机器指令相对OAT头的偏移位置。将这两者相加,就可以得到一个类方法的本地机器指令在内存的位置。我们可以通过调用OatMethod类的成员函数GetCode来获得这个结果。
OatMethod类的成员函数GetQuickCode的实现如下所示(art/runtime/oat_file.h,老罗版本是在.cc文件中,注意区别):
const void* GetQuickCode() const {
return GetOatPointer<const void*>(code_offset_);
}
OatMethod类的成员函数调用另外一个成员函数GetOatPointer来获得一个类方法的本地机器指令在内存的位置。
OatMethod类的成员函数GetOatPointer的实现如下所示(art/runtime/oat_file.h,注意区别):
-
template<class T>
T GetOatPointer(uint32_t offset) const {
if (offset == 0) {
return nullptr;
}
return reinterpret_cast<T>(begin_ + offset);
}