为什么要学习汇编指令
任何高级语言, 最后都是变成汇编指令, 让机器进行执行运算的. 最早的程序也是使用汇编进行编写的.
当我们探究各种编程语言的本质时,总是离不开编程语言最后行程的产物, 也就是汇编命令.
很多时候, 当程序发生一些难以理解的现象的时候, 通过寻常的编程语言的探索已经没有办法找到问题的根源, 这个时候通过汇编研究程序, 可能是另一个方向.
上线的app, 电脑上运行的程序, 已经脱离了原本的编程语言, 都是最终的二进制指令. 因为二进制指令和 汇编是一一对应的. 所以我们可以通过汇编去探究程序的逻辑. 是程序逆向, 黑客不可跳过的一个环节.
arm64
arm64指令集, 目前iOS设备基本都是这个指令集. 模拟器和mac电脑不一样.
如果需要运行,需要在真机上跑程序.
如何学习汇编指令
学习基本的指令与及背景知识. 然后通过c语言的和编译后的汇编的对应关系了解汇编的使用. 然后通过简单的函数编写, 加深理解.
我们可以把水平分成2个层级,
A: 能看懂大概
B: 能够自己实现简单C函数逻辑
但是如果没有自己写过汇编, 是没有办法感受其中的乐趣的.
以下是一个简单的函数, 查看一个字符串里面的左右括号是否匹配. 一下是合法的字符串. "{[()]}" , "()[]{}"
反之,如果左右的括号不对称,则认为该字符串不合法. 比如 "}{}" "{(})" 等
.text // 说明这是一个指令段,
.global _isValidBrace // 声明是向外面暴露的符号, 外面可以通过调用 isValidBrace 使用.
// 可以认为这个汇编.s文件,就是下面这个c函数的替代. 互相是等价的.
// 当然这是实现, 外面使用还需要对这个c函数进行声明, 否则编译就开始报错了.
//BOOL isValidBrace(char *s){
// unsigned long len = strlen(s);
// char *str = calloc(len + 1, 1);
// int lastL = -1;
// for (unsigned long i=0; i<len; i++) {
// char c = s[i];
// if (c == '(' || c== '{' || c== '[') {
// lastL ++;
// str[lastL] = c;
// }else{
// if (lastL < 0) {
// return false;
// }
// char l = str[lastL];
// if ((l=='(' && c==')') || (l=='{' && c=='}') || (l=='[' && c==']') ){
// str[lastL] = '\0';
// lastL --;
// }else{
// return false;
// }
// }
// }
// if(lastL>=0){
// return false;
// }
// if(str){
// free(str);
// str = NULL;
// }
// return true;
//}
// 40 // 这几个数字分别是 ()[]{} 6个字符对应的ASCII码对应的数字
// 41
// 91
// 93
// 123
// 125
//BOOL isValidBrace(char *s);
_isValidBrace:
// 从x0, 到 x30 都是arm64里面的寄存器. 我们可以理解为是机器层面的全局变量.
// stp 是保存指令, store register pair, 同时保存两个寄存器.
// 相近的 str 也是保存指令, store register. 只能保存一个寄存器.
// str x0, [x1] 把 x0的数据 8个字节 写入到x1寄存器的数值对应的地址开始的8个字节.
// stp x29,x30,[sp,#-0x10] 保存x29, x30两个寄存器内数据到 sp-0x10 对应的内存地址里
// stp x29,x30,[sp,#-0x10]! 保存结束后, sp寄存器内数据减0x10
// x86的寄存器是2个字节, w0 是4个字节, x0是8个字节.
// x29 = fp 是栈底指针, x30 =lr 返回地址指针.(也就是ret跳转的地址)
// x19~x28 临时寄存器, 使用前需要保存. 不用担心调用别人的程序,这些数据会被修改. 所以是程序内安全的.
// x9~x15, 可变寄存器, 使用的时候不需要保存. 但是在调用函数后, 数据可能会被修改. 程序内是不安全的
// x0~x7 用于传递参数和返回数值. 超过数量的参数通过栈传递. 通过用x0传递返回值.
stp x29,x30,[sp,#-0x10]! // x29, x30 是
stp x21,x22,[sp,#-0x10]! // 后面使用x19到x22作为临时变量, 所以先保存原始数值, 程序返回前恢复.
stp x19,x20,[sp,#-0x10]!
stp x0,x1,[sp,#-0x10]!
bl _strlen // 调用 strlen 系统函数, 参数 char * s 通过x0 传入, 返回数值保存在 x0.
mov x19,x0
// "#"跟数字 ,标识自然数.
add x0,x0,#1 // >> x0 = x0+1
mov x1,#0 // >> x1 = 0
bl _calloc
mov x21,x0
stp x19,x21,[sp,#-0x10]! // 保存字符串长度 ; 新增加的字符串.
mov x19,#-1 // 记录最后一个字符的位置.
mov x20,#0
loop_str_ce: // 标签, 用于b跳转的时候定位
// ldr=> load register 从内存中加载数据到寄存器,
ldr x0,[sp,#0x10] // 从当前的栈顶地址 +0x10的位置 读取8个字节数据, 赋值给x0.
// 因为这个时候需要读取函数的第一个参数, 需要重新读取, 防止x0已经被污染.
add x0,x0,x20
mov x1,#0
// ldrb => load register byte. 读取一个字节. 每个字符是1个字节
ldrb w1,[x0] // 第i个字符
mov x21,x1 // 记录当前的字符. x21 = x1
cmp x1,#40 // 比较x1和#40两个数字
b.eq equal_lbrace_ce // 如果相等, 就跳转到标签equal_lbrace_ce , 否则继续往后执行
cmp x1,#91
b.eq equal_lbrace_ce
cmp x1,#123
b.eq equal_lbrace_ce
match_rbrace_ce:
cmp x19,#0
b.lt err_ret_ce // 如果小于, 跳转, 否则继续往后执行
ldr x0,[sp,#0x8] // 新字符串指针, 指向首字符. => (sp+0x8) 内存地址对应的数据读取给x0
add x0,x0,x19 // 目标支付地址
ldrb w1,[x0]
cmp w1,#40
b.ne secCmp_ce
cmp x21,#41
b.ne err_ret_ce
b match_ce
secCmp_ce:
ldr x0,[sp,#0x8] // 新字符串指针, 指向首字符.
add x0,x0,x19 // 目标支付地址
ldrb w1,[x0]
cmp w1,#91
b.ne thirdCmp_ce
cmp x21,#93
b.ne err_ret_ce // 不想等时跳转
b match_ce // 跳转到标签, 无条件.
thirdCmp_ce:
ldr x0,[sp,#0x8] // 新字符串指针, 指向首字符.
add x0,x0,x19 // 目标支付地址
ldrb w1,[x0]
cmp w1,#123
b.ne err_ret_ce
cmp x21,#125
b.ne err_ret_ce
b match_ce
match_ce:
mov x1,0
strb w1,[x0]
sub x19,x19,#1 // 减法 => x19 = x19 - #1
b continu_ce // 跳转到标签, 无条件.
equal_lbrace_ce:
add x19,x19,#1
ldr x0,[sp,#0x8] // 新字符串指针, 指向首字符.
add x0,x0,x19 // 目标支付地址
strb w1,[x0] //保存 w1寄存器的数据最后一个字节 到x0对应的地址
continu_ce:
add x20,x20,#1
ldr x0,[sp]
cmp x20,x0
b.lt loop_str_ce
cmp x19,#0
b.lt nor_ret_ce
err_ret_ce:
ldr x0,[sp,#0x8]
bl _free
mov x0,#0
b last_ret
nor_ret_ce:
ldr x0,[sp,#0x8]
bl _free
mov x0,#1
last_ret:
add sp,sp,#0x40
ldp x29,x30,[sp],#0x10
ret