与其他教科书一样,在介绍一门汇编语言之言,肯定会拿寄存器与内存进行对比。那么,寄存器与 内存相比,到底有哪些好处呢?
很直观的优势:
速度;
还是速度
riscv的寄存器有多少个?32个(x0-x31),虽然多一点有可能会保存更多的变量, 但是这样一来所有的变量读取的速度都会降低。
在这张著名的小册子 我们可以看出来,riscv的寄存器命名为x0-x31,但是其ABI的名字不一样。
现在的任务就是在最短的时间内记住这几个寄存器的用途、ABI以及由谁负责保存。
s0-s1 <==> x8-x9
s2-s11 <==> x18-x31
ABI <==> registers
t0-t2 <==> x5-x7
t3-t6 <==> x28-x31
riscv寄存器没有特定的类型,他的操作取决于自身内部的内容。
在一个计算机系统中,我们大体上可以分为三大部分:
op dst, src1, src2
一个操作一条指令。c语言中的运算符在这里也是可以用的.
Assume here that the variables a, b, c are assigned to regs s1, s2, s3.
a = b + c; // C-style
add s1, s2, s3 // riscv assembly
a = b - c ; // c-style
sub s1, s2, s3 // riscv
Suppose :
/*
* a->s0, b->s1, c->s2, d->s3, e->s4
* to complte :
* a = (b + c) - (d + e)
* in riscv
*/
add t1, s1, s2 # t1 = b + c
add t2, s3, s4 # t2 = d + e
sub s0, t1, t2 #
riscv的汇编可以使用”#”进行注释。
这个寄存器很重要,也就是大名鼎鼎的0寄存器,有了它,很多操作就可以 简化了。因为他的内容永远为0,假设,目的地址为这个寄存器的话,操作 就相当于进了黑洞,没有任何影响,其ABI(zero)。
add s3, x0, x0 # c = 0
add s1, s2, x0 # a = b(初始值借用上面的)
格式:
opi dst, src, imm
很显然,这里在op后面有一个i,意味着操作符的对象为立即数。
addi s1, s2, 5 # a = b + 5
addi s3, s3, 1 # c ++
但是没有subi
,我估计是为了防止减法溢出吧。
主要使用两个store(to) 和 load(from).riscv指令只操作在寄存器上, 如果想借用内存的内容,就得需要store和load指令了。
memop reg, off(bAddr)lb t0, 8(sp) # Loads (dereferences) from memory address (sp + 8) into register
# t0.
# lb = load byte, lh = load halfword, lw = load word, ld = load doubleword
sb t0, 8(sp) # Stores (dereferences) from register t0 into memory
# address (sp + 8) sb = store byte, sh = store halfword,
# sw = store word, sd = store doubleword.
sub a0, t0, t1 # t0 - t1 ==> a0
mul a0, t0, t1 # t0 * t1 ==> a0
div a1, s3, t3 # s3/t3 ==> a1
rem a1, s3, t3 # s3/t3 ==> a1
and a3, t3, s3 # t3 & s3 ==> a3
or a3, t3, s3 # t3 | s3 ==> a3
xor a3, t3, s3 # t3 ^ s3 ==> a3
也就是实际不存在,在实际例子中,会自动转化成已有的指令。
riscv的浮点指令前面加上一个 f
. 请熟悉以下几个指令:
fld # float load double
fsw # float store word
当然还可以通过后缀指定单(.s) 双精度(.d).
# load a double-precision value
flw ft0, 0(sp)
# ft0 now contains whatever we loaded from memory + 0
flw ft1, 4(sp)
# ft1 now contains whatever we loaded from memory + 4
fadd.s ft2, ft0, ft1
# ft2 is now ft0 + ft1
单双精度之间也可以进行转换: fcvt.d.s
(convert from single into double)
fcvt.s.d
(convert from double to single)
beq # if equal
bne # if not equal
bgt # greater than
bge # greater than or equals
blt # less than
ble # less than or equals
下面来看一个例子:
# t0 = 0
li t0, 0
li t2, 10
loop_head:
bge t0, t2, loop_end
# Repeated code goes here
addi t0, t0, 1
loop_end:
对应的代码如下:
for (int i = 0;i < 10;i++) {
// Repeated code goes here.
}
这里具体以beq为例:
资料 从 spec 中我们可以得知,branch的指令格式为:
beq rs1, rs2, offset <==> if (rs1 == rs2) pc += sext(offset)
sext的意思是符号位扩展, 如果你具体看一下这条语句的spec的时候,会发现一些有意思的事情,(从右至左)7 bit opcode ,5 bits(offset[4:1]|[11]), 3 bits func3(000暗示BEQ), 5 bits rs1, 5 bits rs2, 7 bits (offset[12]|[10:5]).
这里的offset你可以看成立即数,12 bit的立即数被编码成了13 bits,但是最低位一直为0, 故我们拆分的是去除最低位后的12 bits,上面括号部分已经指明了这个拆分,因为这个blog 无法上传图片,故需要图片的话,可以访问上面的链接。
默认为ra。
stack被用来存储局部变量,这里有一点需要牢记的是,栈是从栈底(高地址)向栈顶 生长(低地址)
| |low<---sp(2)
| |
|____|high<--sp(1)
这里的sp并不一定准确,一般来说都是从栈底开始,栈必须以8 bytes的倍数对其。 一个简短的程序段:
addi sp, sp, -8
sd ra, 0(sp)
call printf
ld ra, 0(sp)
addi sp, sp, 8
ret
const_cast这个强制转换的作用需要你对关键词const
的威力有一个清晰的认识才可以。简而言之
const_cast可以转变由const修饰的某些东西。
必须在定义的时候进行初始化。
const int b = 10;
b = 0; // errors: b can not be changed
const string s = "vimer";
const int i, j = 0; // uninitialized const i
显示的错误为:
vimer@host:~/src/test/c++$ g++ -g const2.cpp -o s2
const2.cpp: In function ‘int main()’:
const2.cpp:18:6: error: assignment of read-only variable ‘b’
b = 0; // errors: b can not be changed
^
const2.cpp:20:12: error: uninitialized const ‘i’ [-fpermissive]
const int i, j = 0; // uninitialized const i
^
这一块在当时看c++的教科书时就很迷惑,现在终于有机会整理了,总结下。
const char *a;
char const *b;
char * const c;
const char* const a;
现在就根据这篇文章
处理此类的困惑,要结合从右到左的(from-right-to-left) 原则。
int * mutable_pointer_to_mutable_int;
int const * mutable_pointer_to_const_int;
int * const const_pointer_to_mutable_int;
int const * const const_pointer_to_const_int;
const int *foo; // equilant to int const *foo
foo
points (\*)
to an int
that cannot change const
.
*foo = 123 or foo[0] = 123; // invalid
foo = &bar; // ok
第二:
int *const foo; //
Means “foo cannot change (const) and points (*) to an int”
*foo = 123 or foo[0] = 123 // ok ==
foo = &bar; // not right
第三:
const int *const foo; //
*foo = 123 or foo[0] = 123; // not right
foo = &bar; // not right
这里会涉及以下三种情况:
const int fun1(); // 无意义,返回值要给其他使用
const int* func2(); // 指针指向的内容不变
int *const func3(); // 指针本身不可变
以上例子不够详细,来看一下更复杂的。
char *Function1()
{ return “Some text”;}
// 如果使用的下面的语句就会crash(我现在还不明白)
Function1()[1]=’a’;
// 如果使用const,编译器就会报错
const char *Function1()
{ return "Some text";}""}
函数参数使用const修饰,这里涉及到了两种情况:
void func(const int a); // a本身是形参,不会被改变
void func2(int *const var); // 指针本身不可变
这里涉及到一个基本的事实,就是函数被调用会开辟一个新栈,其形参会被复制给临时变量,而且 对临时变量所做的修改,都不会影响到形参的内容。
void string_copy(char *dst, const char *src);//
// 函数体内如果修改src 的内容将会编译出错
void func(A a)
其实这里应该同1是一样的。在c/c++中,函数的参数被copy,这样如果 不利用特殊手段,函数的参数本身不会被函数的行为所改变。
void change_arg_value(int val){
val = 56;// val will not changed into 56
}
在c++中,有一个很迷惑人的词语叫做”引用”,初学者,尤其从c语言转过来的 很难接受,至少对我而言是这样。
void change_arg_value2(int &arg2){
args2 = 56;//ok, agrs will be 56
}
那么,在c中相似的用法想必大家都知道了,就是传递地址的方式:
void change_arg_value3(int *args3){
*args3 = 78;
}
二者的对比程序如下:
void change(int &args){
args = 56;
}
void change2(int *args3){
*args3 = 78;
}
int main()
{
int a = 6;
change(a);
cout << "after reference a is " << a << endl;
change2(&a);
cout << "After passing of address of variable, a is " << a << endl;
}
/* output:
* vimer@host:~/src/test/c++$ ./ref
* after reference a is 56
* After passing of address of variable, a is 78
*/
那么,const又是如何与这块签上关系的呢? 对于c++而言,我们知道了引用是一个高效的 “传递参数”的用法,至少不会产生大量的构造、copy相关的操作,这一点尤其对自定义的 class或者struct重要。
void func4(big_struct_type &args4);
好,对于提高参数传递效率的目的我们已经达到了,那为什么还需要const
呢?试想一下,
如果我们对于这个参数只是使用,对,使用,我们不希望在这个函数对这个数据进行修改,
或者不允许对此进行修改,怎么办?const的意义就在于此。
void func5( big_strcut_type const &args5) // 或者
void func5(const big_struct_type &args5)
这块的内容也很复杂,我这里先说明一种最重要的使用案例吧,至少很好理解的。
class Test(){
void method1();
int member_var;
}
除去那些很复杂的方法,如果这里的method1()本意是不修改任何成员变量,那么,你能保证类似 下面的代码不发生吗?
void Test::method1(){
member_var += 1;
}
为了阻止像这样可能出现意外的情况,那么,最好使用
class Test(){
void method1() const;
int member_var;
}
最好的解释还是在这个SO
在一个方法后面加上关键词const,可以有效的防止该方法对类中的变量进行修改,如果这么做的话 编译器也会抛出一个编译错误的。
从另一个角度看,const加载函数结尾为什么会产生这个效果?这里就涉及到了this指针。
int Foo::Bar(int random_arg); // 更详细的如下:
int Foo_Bar(Foo* this, int random_arg);
Foo f;
f.Bar(4); //等同于
Foo f;
Foo_Bar(&f, 4);
但是如果添加const进行修饰,则会有:
int Foo::Bar(int random_arg) const;
int Foo_Bar(const Foo* this, int random_arg);
const影响成员变量的读写属性还是通过*this指针确定的。
在c++11中,使用mutable
关键词可以消除这种限制。
既然我们已经知道const
的好处,尤其在使变量常量化这一点上,是不是与
关键词define
很相似,那么,这二者还是有很大的不同的。
static_cast<type> 或者
static_cast <type-id> ( expression )
截止到目前c++,一共有
1. static cast
2. Dynamic Cast
3. Const Cast
4. Reinterpret Cast
今天介绍的Static Cast
是最简单的一种类型转换声明。其实,在c语言中大部分的类型转换(隐式或者显式)都是类似这样的:
int main(){
float f = 3.5;
int a = f; // in c-sytle conversion
int b = static_cast<int>(f);
cout << "a is " << << " " << a << "b is " << b << endl;
}
那么,使用这个显式转换有什么好处吗?结合第二个例子看一下:
int main(){
int a = 10;
char c = 'a';
int* q = (int*)&c;
int* p = static_cast<int*>(&c);
return 0;
}
输出:
vimer@host:~/src/test/c++$ g++ -g s_cast2.cpp -o s2
s_cast2.cpp: In function ‘int main()’:
s_cast2.cpp:15:31: error: invalid static_cast from type ‘char*’ to type ‘int*’
int* p = static_cast<int*>(&c);
^
上例中的int \*q
,把字符变量的地址指向int型的指针(也许这种转换在程序中就是无意义的),如果加上static_cast<>
就可以让编译器之处这种错误了。
至于这种错误有什么影响,我自己还真菜,没有想清楚,看看这个so
给出的解释是c是占据一个字节,但是指针是4个字节,这样在运行时就会出错。(至于为什么我还有点难以理解)
static_cast
可以在编译阶段显式的阻止不正确的类型转换,这一特点很重要,试想在一个规模很
庞大的c++工程中,比如aosp这种,前后自定义的类型特别多,很容易在类型转换的时候导致一些
很微小的、不易察觉的错误。
第二个例子:
class Int{
int x;
public:
Int(int x_in = 0) : x {x_in} {
cout << "Conversion Ctor called" << endl;
}
operator string(){
cout << "Conversion Operator " << endl;
return to_string(x);
}
};
int main(){
Int obj(3);
string str = obj;
obj = 20;
string str2 = static_cast<string>(obj);
obj = static_cast<Int>(30);
return 0;
}
output:
vimer@host:~/src/test/c++$ ./class
Conversion Ctor called
Conversion Operator
Conversion Ctor called
Conversion Operator
Conversion Ctor called
来看最后一个例子.
using namespace std;
struct B {
int m = 0;
void hello() const {
std::cout << "hello world, this is B\n";
}
};
struct D: B {
void hello() const {
std::cout << "hello world , this is D\n";
}
};
enum class E { ONE = 1, TWO, THREE };
enum EU { ONE = 1, TWO, THREE };
int main(){
// 1 initializing conversion
int n = static_cast<int>(3.14);
std::cout <<"n = " << n << endl;
std::vector<int> v = static_cast<std::vector<int>>(10);
std::cout << "v.size() = " << v.size() << endl;
// 2 static downcast
D d;
B& br = d;
br.hello();
D& another_d = static_cast<D&>(br);
another_d.hello();
// 3 lvalue to xvalue
std::vector<int> v2 = static_cast<std::vector<int>&&>(v);
std::cout << "after move, v.size() is " << v.size() << endl;
// 4 discard-value expression
static_cast<void>(v2.size());
// 5 inverse of implicit
void* nv = &n;
int* ni = static_cast<int*>(nv);
std::cout << "*ni = " << *ni << endl;
// 6 array-to-pointer followed by upcast
D a[10];
B* dp = static_cast<B*>(a);
// 7 scoped enum to int or float
E e = E::ONE;
int one = static_cast<int>(e);
std::cout << one << endl;
// 8 int to enum, enum to another enum
E e2 = static_cast<E>(one);
EU eu = static_cast<EU>(e2);
// 9. pointer to member upcast
int D::*pm = &D::m;
std::cout << br.*static_cast<int B::*>(pm) << endl;
// 10. void* to any type
void* voidp = &e;
std::vector<int>* p = static_cast<std::vector<int>*>(voidp);
}
vimer@host:~/src/test/c++$ g++ -g cast.cpp -o cast
vimer@host:~/src/test/c++$ ./cast
n = 3
v.size() = 10
hello world, this is B
hello world , this is D
after move, v.size() is 0
*ni = 3
1
0
等我将这四种类型转换介绍完毕后我再统一介绍下这四种类型的适用场景,这里先说一下 static_cast与c-style的不同:
与下面的upcast类似,都是用于cast方面的内容。downcast就是由基类向派生类 cast.
D& another_d = static_cast<D&>(br);
another_d.hello();
就是downcast转换。
D d;
B& br = d;
br.hello();
而这种就是upcast,有派生类向基类cast.
估计我们在学习c++基础教程的时候,都会遇到在操作符重载这节中使用的实数和虚数的概念, 今天,我也在这里稍微简单地记录下这个问题。也算弥补自己在这方面没有记录的一个遗憾吧。
当然是使用这个大名鼎鼎的用例了。
#include <iostream>
using namespace std;
class Complex {
private:
int real, imag;
public:
Complex(int r = 0, int i = 0) {
real = r;
imag = i;
}
Complex operator + (Complex const &obj){
Complex res;
res.real = real + obj.real;
res.imag = imag + obj.imag;
return res;
}
void print() { cout << real << "+i " << imag << endl ;}
};
int main()
{
Complex c1(10, 5), c2(2, 4);
Complex c3 = c1 + c2;
c3.print();
}
从上面的例子中,自己在c++目前存在的缺陷,还是集中在参数的引用等方面。
其实,这是我在网上看到这个标题后感觉还是挺有意思的,后面一看,才知道是 构造函数呀!
#include <iostream>
using namespace std;
class dis_time{
public:
dis_time(int h, int m, int s){
hour = h;
minute = m;
second = s;
}
void print(){
cout << hour <<":" << minute <<":" << second << endl;
}
private:
int hour;
int minute;
int second;
};
int main()
{
dis_time t1(1,1,1);
t1.print();
}
这是我的第一版实现(不考虑全面)。后面,如果我打算使用
dis_time t2;
t2();
则会报下面的错误:
oper2.cpp: In function ‘int main()’:
oper2.cpp:28:11: error: no matching function for call to ‘dis_time::dis_time()’
dis_time t1;
^~
oper2.cpp:11:3: note: candidate: dis_time::dis_time(int, int, int)
dis_time(int h, int m, int s){
^~~~~~~~
oper2.cpp:11:3: note: candidate expects 3 arguments, 0 provided
oper2.cpp:9:7: note: candidate: constexpr dis_time::dis_time(const dis_time&)
class dis_time{
^~~~~~~~
oper2.cpp:9:7: note: candidate expects 1 argument, 0 provided
oper2.cpp:9:7: note: candidate: constexpr dis_time::dis_time(dis_time&&)
oper2.cpp:9:7: note: candidate expects 1 argument, 0 provided
oper2.cpp:29:10: error: no match for call to ‘(dis_time) (int, int, int)’
t1(2,4,5);
这里主要要介绍的是”«“输出运算符的重载,其实,是很简单的一个实例,但是具体来讲还是有一个地方需要重点补充下。
class Complex
{
double real,imag;
public:
Complex( double r=0, double i=0):real(r),imag(i){ };
friend ostream & operator<<( ostream & os,const Complex & c);
friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
return os;
}
C++中输入运算符的重载,第一个参数需要是ostream的引用,第二个参数就是需要运用 重载符的对象,最好也是引用形式,而且,在输出时不能改变原有的对象,所以我们可以 添加const关键词进行修饰。
这段代码摘自here,我们可以很容易的 看出,这里的操作符是在类外面进行重载的。这样的情况下,需要在类内的成员中添加 友元函数声明,不然编译器会报错的。
根据这篇文章, 友元函数的特性是可以把一个类的成员分享给其他类, 实现类共享,此类代码在 aosp中也不少的, 主要是为了减少不必要的开销, 提高效率。但是缺点就是破坏了类的 封装性,这一点还没有深刻的体会。
那么, 既然已经很明确建议不适用友元函数,在重载输出运算符时有没有好的办法呢?有的:
#include <iostream>
using namespace std;
class Person {
public:
Person(const string& first_name, const string& last_name) : first_name_(first_name), last_name_(last_name) {}
const string& get_first_name() const {
return first_name_;
}
const string& get_last_name() const {
return last_name_;
}
private:
string first_name_;
string last_name_;
};
// Call Person's some method to avoid friend.
ostream& operator<<(ostream& os, const Person& p){
os <<"first_name=" << p.get_first_name() <<"," <<"last_name=" << p.get_last_name();
return os;
}
int main() {
string first_name, last_name, event;
cin >> first_name >> last_name >> event;
auto p = Person(first_name, last_name);
cout << p << " " << event << endl;
return 0;
}
public class JavaToDex{
public static void main(String[] args){
System.out.println("How to make java to dex");
}
}
保存文件需要注意,要以类名进行结尾,这是java语言编程的一个特色。
使用javac命名转化java程序转化为class文件。
javac JavaToDex.java ==> JavaToDex.class
java JavaToDex ==> output sth
注意,这个工具并在aosp下,其实想一想也是,dx是为了将java转化dex文件,那么,至少这个工作是由sdk提供的。一般来说,这个工具存放在build-tools下。
usage:
dx --dex [--debug] [--verbose] [--positions=<style>] [--no-locals]
[--no-optimize] [--statistics] [--[no-]optimize-list=<file>] [--no-strict]
[--keep-classes] [--output=<file>] [--dump-to=<file>] [--dump-width=<n>]
[--dump-method=<name>[*]] [--verbose-dump] [--no-files] [--core-library]
[--num-threads=<n>] [--incremental] [--force-jumbo] [--no-warning]
[--multi-dex [--main-dex-list=<file> [--minimal-main-dex]]
[--input-list=<file>] [--min-sdk-version=<n>]
[--allow-all-interface-method-invokes]
[<file>.class | <file>.{zip,jar,apk} | <directory>] ...
Convert a set of classfiles into a dex file, optionally embedded in a
jar/zip. Output name must end with one of: .dex .jar .zip .apk or be a
directory.
Positions options: none, important, lines.
--multi-dex: allows to generate several dex files if needed. This option is
exclusive with --incremental, causes --num-threads to be ignored and only
supports folder or archive output.
--main-dex-list=<file>: <file> is a list of class file names, classes
defined by those class files are put in classes.dex.
--minimal-main-dex: only classes selected by --main-dex-list are to be put
in the main dex.
--input-list: <file> is a list of inputs.
Each line in <file> must end with one of: .class .jar .zip .apk or be a
directory.
--min-sdk-version=<n>: Enable dex file features that require at least sdk
version <n>.
dx --annotool --annotation=<class> [--element=<element types>]
[--print=<print types>]
dx --dump [--debug] [--strict] [--bytes] [--optimize]
[--basic-blocks | --rop-blocks | --ssa-blocks | --dot] [--ssa-step=<step>]
[--width=<n>] [<file>.class | <file>.txt] ...
Dump classfiles, or transformations thereof, in a human-oriented format.
dx --find-usages <file.dex> <declaring type> <member>
Find references and declarations to a field or method.
<declaring type> is a class name in internal form, like Ljava/lang/Object;
<member> is a field or method name, like hashCode.
dx -J<option> ... <arguments, in one of the above forms>
Pass VM-specific options to the virtual machine that runs dx.
dx --version
Print the version of this tool (1.16).
dx --help
Print this message.
上面的这个是帮助文档,值得需要注意下的。 Convert a set of classfiles into a dex file, optionally embedded in a jar/zip. Output name must end with one of: .dex .jar .zip .apk or be a directory.
由此看来,dx可以转化的文件格式还是很多的。
dx --dex --output=JavaToDex2.dex JavaToDex.class
就会由dex文件生成class文件。
好了, 生成dex文件只是万里长征的第一步,后面才是真正的旅途。
如果想查看dex文件的格式(如果有必要的话,这一步为了研究art的东西,一定需要研究这个文件)
有一个比较好的东西是dexdump,唉,这个工具又和dx不同了,dexdump 看样子在aosp中需要的很多,那么,这个文件会直接生成在out/host/linux-x86/bin下。
dexdump -h
dexdump: no file specified
Copyright (C) 2007 The Android Open Source Project
dexdump: [-c] [-d] [-f] [-h] [-i] [-l layout] [-m] [-t tempfile] dexfile...
-c : verify checksum and exit
-d : disassemble code sections
-f : display summary information from file header
-h : display file header details
-i : ignore checksum failures
-l : output layout, either 'plain' or 'xml'
-m : dump register maps (and nothing else)
-t : temp file name (defaults to /sdcard/dex-temp-*)
我们来看看上面那个文件的具体展示:
vimer@host:~/src/aosp/out/host/linux-x86/bin$ dexdump JavaToDex.dex
Processing 'JavaToDex.dex'...
Opened 'JavaToDex.dex', DEX version '035'
Class #0 -
Class descriptor : 'LJavaToDex;'
Access flags : 0x0001 (PUBLIC)
Superclass : 'Ljava/lang/Object;'
Interfaces -
Static fields -
Instance fields -
Direct methods -
#0 : (in LJavaToDex;)
name : '<init>'
type : '()V'
access : 0x10001 (PUBLIC CONSTRUCTOR)
code -
registers : 1
ins : 1
outs : 1
insns size : 4 16-bit code units
catches : (none)
positions :
0x0000 line=1
locals :
0x0000 - 0x0004 reg=0 this LJavaToDex;
#1 : (in LJavaToDex;)
name : 'main'
type : '([Ljava/lang/String;)V'
access : 0x0009 (PUBLIC STATIC)
code -
registers : 3
ins : 1
outs : 2
insns size : 8 16-bit code units
catches : (none)
positions :
0x0000 line=3
0x0007 line=4
locals :
Virtual methods -
source_file_idx : 2 (JavaToDex.java)
这个也是后续研究dex2oat的基础。