我们都知道,aosp中的对java代码的运行与JVM是不一样,这一切都是由一个叫做art的虚拟机完成的(并不包括从java到dex这一过程)。 也就是可以认为, art的输入是dex指令,输出的针对相应平台的汇编代码,
具体而言, art编译优化器的输入是APK中的DEX字节码, 输出是优化后的HInstruction.
这篇笔记就简单介绍一下这个过程,主要是先介绍概念,然后再介绍aosp中的对应代码名称,用来混个脸熟。主要参考了邓凡平老师的 «深入理解Android Java虚拟机»的第六章。看过这本书的都知道, 第六章首先介绍了一些编译原理的概念,比如语法分析、词法分析, 当然,没有实践就没有发言权,这篇文章就不做说明了。
dex文件是由class文件(或者apk文件)经过dx工具生成的。那么,我们就首先从dex文件入手。
…
抱歉,这一部分没啥可说的,如果执意探究下aosp的dex的相关细节, 我也记录了一些,在这里
按照文章的说法, 编译器在某个阶段产生IR, 比较复杂, 后面补充详细的。
java字节码就是一种单地址的IR, 单地址的IR中指令的操作数和操作结果都存在于栈中。注意一点,art在做编译优化时并不是直接针对dex字节码的,而是使用了 自定义的中间表达式。
art优化器输入的是dex字节码, 输出后的是优化的Hinstruction.
HGraph
代表CFG。声明在art/compiler/optimizing/nodes.h文件中。
基本块包含一行或多行代码,没有跳转分支代码。相当于图的结点(node). 在art中的代表是HBasicBlock
. HBasicBlockBuilder
是构造CFG的辅助类。
数据流分析依赖于CFG
HInstructionBuilder::Build[art/compiler/optimizing/instruction_builder.cc] 还是先判断下基本块的类型:
if (current_block_->IsEntryBlock()) { // 入口基本块
InitializeParameters();
AppendInstruction(new (allocator_) HSuspendCheck(0u)); // 为什么加这条
AppendInstruction(new (allocator_) HGoto(0u));
continue;
} else if (current_block_->IsExitBlock()) { // 出口基本块
AppendInstruction(new (allocator_) HExit());
continue;
} else if (current_block_->IsLoopHeader()) { // 循环块
HSuspendCheck* suspend_check = new (allocator_) HSuspendCheck(current_block_->GetDexPc());
current_block_->GetLoopInformation()->SetSuspendCheck(suspend_check);
// This is slightly odd because the loop header might not be empty (TryBoundary).
// But we're still creating the environment with locals from the top of the block.
InsertInstructionAtTop(suspend_check);
}
嗨 这篇文章没有太多干货,主要是简单记录下art中的一些术语, 当然是在看邓书的时候记下来的。
aosp 7中据说拥有13种IR优化的方法, HDeoptimize(反优化)或者说以interpreter方式来执行。
这个大类声明在art/compiler/optimizing/locations.h文件中,它包含了方方面面的任务。 比如IR对象的输入输出操作数来自寄存器还是内存栈、以何种方式进行解析、目标平台等等如此多的作用 都可以被LocationSummary进行封装。当然它需要被 code_generation.cc[并不一定是这个文件类似的]文件消费。
下面我们摘取几处代码进行剖析:
// locations.h ::
class LocationSummary : public ArenaObject<kArenaAllocLocationSummary> {
public:
enum CallKind { // CallKind 描述了一个Hinstruction是否涉及函数调用
kNoCall, // 该Hinstructon不涉及函数调用,即不与HInvoke相关
kCallOnMainAndSlowPath,
kCallOnSlowPath, // 有些java代码需要不优化的方式运行,除0抛异常等
kCallOnMainOnly // 函数调用
};
explicit LocationSummary(HInstruction* instruction,
CallKind call_kind = kNoCall,
bool intrinsified = false);
void SetInAt(uint32_t at, Location location) { // 指定IR的入口, Location对象后面会进行介绍
inputs_[at] = location;
}
Location InAt(uint32_t at) const {
return inputs_[at];
}
size_t GetInputCount() const {
return inputs_.size();
}
void SetOut(Location location, Location::OutputOverlap overlaps = Location::kOutputOverlap)
// ... 比较特殊,需要判断输出是否对输入寄存器进行了覆盖
void UpdateOut();
void AddTemp();
void AddRegisterTemps();
// ...
我们在上面的代码中看到了这里大量使用了Location这个对象, 我们简单来看一下这个数据结构。同样, 该结构也是定义在相同的头文件中。
/**
* A Location is an abstraction over the potential location
* of an instruction. It could be in register or stack.
*/
class Location : public ValueObject {
public:
enum OutputOverlap {
// The liveness of the output overlaps the liveness of one or
// several input(s); the register allocator cannot reuse an
// input's location for the output's location.
kOutputOverlap,
// The liveness of the output does not overlap the liveness of any
// input; the register allocator is allowed to reuse an input's
// location for the output's location.
kNoOutputOverlap
};
enum Kind {
kInvalid = 0,
kConstant = 1,
kStackSlot = 2, // 32bit stack slot.
kDoubleStackSlot = 3, // 64bit stack slot.
kRegister = 4, // Core register.
// We do not use the value 5 because it conflicts with kLocationConstantMask.
kDoNotUse5 = 5,
kFpuRegister = 6, // Float register.
kRegisterPair = 7, // Long register.
kFpuRegisterPair = 8, // Double register.
// We do not use the value 9 because it conflicts with kLocationConstantMask.
kDoNotUse9 = 9,
kSIMDStackSlot = 10, // 128bit stack slot. TODO: generalize with encoded #bytes?
// Unallocated location represents a location that is not fixed and can be
// allocated by a register allocator. Each unallocated location has
// a policy that specifies what kind of location is suitable. Payload
// contains register allocation policy.
kUnallocated = 11,
};
// 后面还有很多成员方法
从注释中我们也可以看出来, Locations的作用对象既可以是寄存器,也可以是stack.
这不是一个文件,各个不同的arch有不同的code_generations的生成文件。我们先练看一下这个文件中的几个有意思的数据结构。
我们以MIPS64为例(art/compiler/optimizing/code_generator_mips64.h),可以由LocationsBuildMIPS64派生出很多与arch相关的IR,这些IR是从dex到assembler的中间件。结合前面的知识, 我们看一下这个东西的大概:
class LocationsBuilderMIPS64 : public HGraphVisitor {
public:
LocationsBuilderMIPS64(HGraph* graph, CodeGeneratorMIPS64* codegen)
: HGraphVisitor(graph), codegen_(codegen) {}
#define DECLARE_VISIT_INSTRUCTION(name, super) \
void Visit##name(H##name* instr) override;
FOR_EACH_CONCRETE_INSTRUCTION_COMMON(DECLARE_VISIT_INSTRUCTION)
FOR_EACH_CONCRETE_INSTRUCTION_MIPS64(DECLARE_VISIT_INSTRUCTION)
#undef DECLARE_VISIT_INSTRUCTION
void VisitInstruction(HInstruction* instruction) override {
LOG(FATAL) << "Unreachable instruction " << instruction->DebugName()
<< " (id " << instruction->GetId() << ")";
}
private:
void HandleInvoke(HInvoke* invoke);
void HandleBinaryOp(HBinaryOperation* operation);
void HandleCondition(HCondition* instruction);
void HandleShift(HBinaryOperation* operation);
void HandleFieldSet(HInstruction* instruction, const FieldInfo& field_info);
void HandleFieldGet(HInstruction* instruction, const FieldInfo& field_info);
Location RegisterOrZeroConstant(HInstruction* instruction);
Location FpuRegisterOrConstantForStore(HInstruction* instruction);
InvokeDexCallingConventionVisitorMIPS64 parameter_visitor_;
CodeGeneratorMIPS64* const codegen_;
DISALLOW_COPY_AND_ASSIGN(LocationsBuilderMIPS64);
};
很明显的是,LocationsBuilderxx是遍历CFG的IR并为他们生成合适的LocationsSummary 的对象。
我们以MIPS64的visitadd函数为例,具体看一下上面两个数据结构的用法。
void LocationsBuilderMIPS64::VisitAdd(HAdd* instruction) {
HandleBinaryOp(instruction);
}
void InstructionCodeGeneratorMIPS64::VisitAdd(HAdd* instruction) {
HandleBinaryOp(instruction);
}
//
void LocationsBuilderMIPS64::HandleBinaryOp(HBinaryOperation* instruction) {
DCHECK_EQ(instruction->InputCount(), 2U);
LocationSummary* locations = new (GetGraph()->GetAllocator()) LocationSummary(instruction);
DataType::Type type = instruction->GetResultType();
switch (type) {
case DataType::Type::kInt32:
case DataType::Type::kInt64: {
locations->SetInAt(0, Location::RequiresRegister()); // 期望第二个操作数为寄存器
HInstruction* right = instruction->InputAt(1);
bool can_use_imm = false;
if (right->IsConstant()) {
int64_t imm = CodeGenerator::GetInt64ValueOf(right->AsConstant());
if (instruction->IsAnd() || instruction->IsOr() || instruction->IsXor()) {
can_use_imm = IsUint<16>(imm);
} else {
DCHECK(instruction->IsAdd() || instruction->IsSub());
bool single_use = right->GetUses().HasExactlyOneElement();
if (instruction->IsSub()) {
if (!(type == DataType::Type::kInt32 && imm == INT32_MIN)) {
imm = -imm;
}
}
if (type == DataType::Type::kInt32) {
can_use_imm = IsInt<16>(imm) || (Low16Bits(imm) == 0) || single_use;
} else {
can_use_imm = IsInt<16>(imm) || (IsInt<32>(imm) && (Low16Bits(imm) == 0)) || single_use;
}
}
}
if (can_use_imm)
locations->SetInAt(1, Location::ConstantLocation(right->AsConstant()));
else
locations->SetInAt(1, Location::RequiresRegister());
locations->SetOut(Location::RequiresRegister(), Location::kNoOutputOverlap);
}
break;
case DataType::Type::kFloat32:
case DataType::Type::kFloat64:
locations->SetInAt(0, Location::RequiresFpuRegister());
locations->SetInAt(1, Location::RequiresFpuRegister());
locations->SetOut(Location::RequiresFpuRegister(), Location::kNoOutputOverlap);
break;
default:
LOG(FATAL) << "Unexpected " << instruction->DebugName() << " type " << type;
}
}
«««< HEAD
同样位于compiler/optimizing/目录下,该文件的作用是将dex代码转变为IR
该目录下有一篇文章专门讲述art虚拟机的问题,这里还是总结下最简单的知识点。
dex2oat处理一个包含classes.dex的jar包或者apk文件时,会生成两个文件,一个.oat文件, 一个.art文件。
因为在目前的项目中,涉及到了很多了int与hex或者bin之间转换,之前还是傻乎乎的打算计算器去手工运算,忽然 之间大佬给了一个c语言的程序,看了以后恍然大悟,原来这个是最简单的方式,
int d = 123;
printf("%d == 0x%x\n", d, d);
// output: 123 == 0x7b
int d = 123;
printf("%d == %x\n", d, d);
// 123 == 7b
可以发现,hex的格式输出符为%x
, 那么如果想打印出前缀0x
,则需要在格式符%x
添加0x
,如果是大写的0X, 需要
大写的0X
.
首先看”%x”更多的用法,标准的%x期待的数据类型是unsigned int.
void show_bytes(unsigned char* start, int len){
int i;
printf("len is %d\n", len);
for (i = 0; i < len; i++){
printf(" 0x%02x", (unsigned int)start[i]); // 有前导0, 两位宽
}
}
int main(){
short x = 12345;
show_bytes((unsigned char *) &x, sizeof(short));
} // output
// len is 2
// 0x39 0x30
如果这里改为int的话, 则会输出4个字节。
int main(){
int x = 12345;
show_bytes((unsigned char *) &x, sizeof(int));
} // output:
// len is 4
// 0x39 0x30 0x00 0x00
int型数据12345的二进制为0b 0011 0000 0011 1001. 这里的细节很重要, 由前面知道, %x的期待数据类型是unsigned int(8 bits), 也就是说, %x的输出位宽就是 8 bits,而一个HEx是4bits, 所以用”0x%02x”就很合适(2代表2位宽). 另一个细节就是, 12345的 二进制改写的hex应该为0x 30 39, 但实际输出确实 0x39 0x30, 也就是数的LSB在高位, 所以这样的情况属于小端机器。
事情的缘由是这样的, 我在编写一个脚本,需要使用两个参数: 一个是java源代码,一个是相应的体系架构。 为了程序的健壮性, 我在调用功能函数之前首先调用检查函数以检查这两个参数的正确性, 下面是代码原型:
function check_args(){
local T=$(gettop)
if [[ ${T} == '' ]]; then
echo " You dont source build/envsetup.sh"
usage_help
return 1
fi
if [[ $# != 2 ]]; then # check num of args
echo "Requies 2 args"
usage_help
return 1 # Can not use source any more
fi
case $2 in
...
*)
echo "Not arch"
usage_help
return 1
那么, 调用过程就是这样:
function run_cmd(){
check_args "$@"
if [[ $? != 0 ]]; then
echo -e "Check failed\n"
return
fi
}
run_cmd "$@" # here is exe cmd in shell script
我第一次写这个代码的时候, 结果一直不对,我也不知道为什么。 后面vscode给我提示了一下,是foo references arguments, but none are ever passed
我瞬间注意到了, 参数的整体应该是$@
.
示例代码:
sayhello() {
echo "Hello $1"
}
sayhello # ./myscript World
vimer@host:~/test$ ./myscript World
hello
# corrent code:
sayhello() {
echo "Hello $1"
}
sayhello "$@"
在一个函数中, “$1”是针对函数的参数,对于脚本的参数,则应该使用 “$@”, 而且, 还也许需要双引号括起来的。
看代码:
options="-j 5 -B"
make $options file
这样是不行的, make在解析的时候会把第一个字符(也许)单独拎出来出来,这是不行的。 后面vscode给我提示了一下,是这样 解决如下:
options=(-j 5 -B) # ksh: set -A options -- -j 5 -B
make "${options[@]}" file # This is OK
make_with_flags() { make -j 5 -B "$@"; }
make_with_flags file # Function , ok
其实这篇文章之所以起这个名字,并不是针对别人的,而是针对我自己的。 我不知道大家有没有一个感觉, 就是shell(主要是bash)一般来说,他的语言规则很弱,什么意思?就是说,这种语言是解释型的,任何的 语法错误只有在运行时才能够打印出来,这样导致的一点就是你无法通过强制的语法记忆去改善这种局面。 更恐怖的是,不同版本的shell更加剧了这种局面。
那么怎么办? github shellcheck是一个很好的shellcheck 的shell 规则检查git repo,我发现这个repo基本概括了shell的一些值得注意的地方,值得关注。
()也是一个数组的形式. 对我来说,我目前的做法就是在脚本中或者子函数中如果要执行相关的命令的时候,一般更倾向使用()的方式,而$()则可以将 函数调用的结果提取出来。比如:
local T=$(gettop) # caller
function gettop(){ # callee
echo $T # ST is string
}
我个人认为()
这种方式比两个反引号的要好一些。$()与` `在语法上等同,都是用来重组命令,但是$()比` `又具有明显的优势与劣势:
1. 多层嵌套$()更清晰一些
2. 形式上更容易
反引号``也不是没有优点: 1. 移植性更好, 绝大部分unix shell都支持反引号(backticks)的用法。bash 确保可以使用$()
哈,看到这个命令,我觉得你就会知道我接下来介绍哪个用法了。对,就是$var.首先我参考的就是OS 我来简单说下。
像这样的变量很容易理解,一般我们打印一个字符串变量、整型变量啥的可以直接打印。
英文的翻译是变量扩展,具体请看下面的解释:
The ‘$’ character introduces parameter expansion, command substitution, or arithmetic expansion.
先看一个基本的参数替换
例子:
val=23 # want 23+abc ==> 23abc
vimer@host:~/src/aosp_art$ echo $valabc
vimer@host:~/src/aosp_art$ echo ${val}abc
23abc
看一个算数替换
的例子,也就是寻找数组的元素,更详细的例子可以看下面()
的操作:
a=( foo bar baz )
vimer@host:~/src/aosp_art$ echo $a[0]
foo[0] # error
vimer@host:~/src/aosp_art$ echo ${a[0]}
foo
后面补充命令替换的。
这个操作不同于前面的替换,更过的是借用其他的操作符号,比如’#’, ‘%’,对相关的变量进行操作。
file=/dir1/dir2/dir3/my.test.txt # define a var
vimer@host:~/$ echo ${file#*/}
dir1/dir2/dir3/my.test.txt # del chars located before in the first char '/'
vimer@host:~$ echo ${file##*/} # del all chars before in the last char '/'
my.test.txt
vimer@host:~$ echo ${file#*.} # del all chars before the first char '.'
test.txt
vimer@host:~$ echo ${file##*.} # del all chars before the last char '.'
txt
vimer@host:~$ echo ${file%/*} # del the last char '/' and after it all chars
/dir1/dir2/dir3
vimer@host:~$ echo ${file%%/*} # del the first char '/' and after it all chars, so it is null
vimer@host:~$ echo ${file%.*} #拿掉最后一个.及其右面的字符
/dir1/dir2/dir3/my.test
vimer@host:~$ echo ${file%%.*} # 拿掉第一个.及其右面的字符
/dir1/dir2/dir3/my
这里细心地读者应该发现了一些特殊的处理细节。${}目前能够处理的位置只能是第一个或者最后一个指定的字符,好,我们这里总结一下:
1. 以”$”为中心, “#”和”%”在键盘上分别位于$
的左右两侧,所以${#}删除指定字符左边的东西;
2. “#”可以看成从左边往右逐个赶变量,且一个”#”表示匹配到第一个字符“#/”前的所有字符;
3. “%”可以看成从右往左检索变量,${file%/},包括这个变量的写法,你也要从右往左写,两个表示贪婪的用法;
4. “*“同其他的用法, 暂且可以看成n多吧。
这一个用法,对于得到一个源文件的basename具有重要的作用。
vimer@host:~$ echo ${file:0:5}
/dir1
vimer@host:~$ echo ${file:5:5}
/dir2
基本就是在指定打印从哪位开始后面连续几位。第一个就是从0位开始连续5位字符;第二个是从第5位开始连续5位。
下面介绍的才是真正的变量替换的用法。
vimer@host:~$ file=/dir1/dir2/dir3/my.test.txt
vimer@host:~$ echo ${file/dir/path} # path will replace the first dir1 chars
/path1/dir2/dir3/my.test.txt
vimer@host:~$ echo ${file//dir/path} # path will replace all dir chars
/path1/path2/path3/my.test.txt
vimer@host:~$ echo ${file//\//-} # same as above
-dir1-dir2-dir3-my.test.txt
vimer@host:~$ echo ${file/\//-}
-dir1/dir2/dir3/my.test.txt
总结就是: 1. ${string/old/new} 这和bash中的正则表达式的用法很像,且一个就替换左边的第一个,两个的话就全部替换。
很简单,就是下面的用法:
vimer@host:~$ echo ${#file}
27
shell中的数组比较特殊, 下面:
vimer@host:~$ A="a b c ef" # only define a string
vimer@host:~$ echo $A # print whole string
a b c ef
vimer@host:~$ A=(a b c ef) # define a char array
vimer@host:~$ echo $A
vimer@host:~$ echo ${A[@]} # 还是与${}配合打印整个数组的内容
a b c ef
vimer@host:~$ echo ${A[*]}
a b c ef
vimer@host:~$ echo ${A[2]} # print the third elemnt
c
vimer@host:~$ echo ${#A[@]} # get num of array, same as ${#file}
4
vimer@host:~$ echo ${#A[3]} # 求某个元素的长度
2
具体可以看一个例子,需要结合上面刚刚介绍的知识。
selection=${choices[$(($answer))]}
这是aosp脚本中的一个语句, 其中 answer 是通过read 命令读取用户的输入的一个数字。
$()可以看成调用函数
() 数组的形式, 也可以是命令集合,与``类似
${}多用于变量的截取 长度 数组的各种操作
$(()) 多用于整型运算
expection在众多的编程语言中占据着重要的地位, 大名鼎鼎的java python都有这个关键词。
expection是一个基类,一般可以通过 #include
一般来说,形式如这个代码块:
try {
bool isValid = checkUsername(username);
if(isValid) {
cout << "Valid" << '\n';
} else {
cout << "Invalid" << '\n';
}
} catch (BadLengthException e) {
cout << "Too short: " << e.what() << '\n';
}
这个catch就是需要自定义的.
#include <exception>
using namespace std;
/* Define the exception here */
class BadLengthException : public exception {
public:
int N;
BadLengthException(int n) {
this->N = n;
};
int what() {
return this->N;
}
};