X86-64和ARM64用户栈的结构 (3) ---_start到main
_start到main()函数之间的栈
1、x86-64
X86-64的寄存器相对于X86有扩展,主要不同体现在:
- 通用寄存器:X86-64有16个64bit通用寄存器
- 状态寄存器:1个64bit状态寄存器RFLAGS,仅仅低32bit被使用
- 指令寄存器:1个64bit指令寄存器RIP
- MMX寄存器:8个64bitMMX寄存器,16个128bitXMM寄存器。当使用这些寄存器时,数据的地址必须对齐到64bit、128bit。
16个64bit寄存器 为:RAX,RBX,RCX,RDX,RDI,RSI,RBP,RSP,R8,R9,R10,R11,R12,R13,R14,R15
在X86-64架构的处理器上,Windows和Linux的函数调用规则是不一样。
1.1、Stack Frame
Linux使用System V Application Binary Interface的函数调用规则。在《System V Applocation Binary Interface》中3.2.2 The Stack Frame中写道:
In addition to registers, each function has a frame on the run-time stack. This stack grows downwards from high addresses. Figure 3.3 shows the stack organization. The end of the input argument area shall be aligned on a 16 (32 or 64, if __m256 or __m512 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32 or 64) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame.
在输入参数的结尾处rsp必须对齐到16字节,当调用函数时,首先rsp会减8,rip会压栈,在栈中占8个字节,然后rip指向另一个函数的entry point,也即控制转移到了函数的entry point。由于rip压栈了,rsp+8应该是16字节对齐。
至于为什么需要16字节对齐?查了一些资料发现和Sreaming SIMD Extensions(SSE)有关,它是一组CPU指令,用于像信号处理、科学计算或者3D图形计算一样的应用(SSE入门)。SIMD 也是几个单词的首写字母组成的: Single Instruction, Multiple Data。 一个指令发出后,同一时刻被放到不同的数据上执行。16个128bit XMM寄存器可以被SSE指令操控,SSE利用这些寄存器可以同时做多个数据的运算,从而加快运算速度。但是数据被装进XMM寄存器时,要求数据的地址需要16字节对齐,而数据经常会在栈上分配,因此只有要求栈以16字节对齐,才能更好的支持数据的16字节对齐。
1.2、Parameter Passing
当参数的数目小于7个时,使用rdi,rsi, rdx, rcx, r8 and r9传递参数,大于等于7个时使用stack传参数。具体的规则见《System V Applocation Binary Interface》中3.2.3 Parameter Passing
- rax 作为函数返回值使用。
- rsp 栈指针寄存器,指向栈顶。
- rdi,rsi,rdx,rcx,r8,r9 用作函数参数,依次对应第1参数,第2参数...
- rbx,rbp,r12,r13,r14,r15 用作数据存储,遵循被调用者(callee)使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
- r10,r11 用作数据存储,遵循调用者(caller)使用规则,简单说就是使用之前要先保存原值
1.3、_start函数
0000000000000540 <_start>: 540: 31 ed xor %ebp,%ebp 542: 49 89 d1 mov %rdx,%r9 545: 5e pop %rsi 546: 48 89 e2 mov %rsp,%rdx 549: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 54d: 50 push %rax 54e: 54 push %rsp 54f: 4c 8d 05 da 02 00 00 lea 0x2da(%rip),%r8 # 830 <__libc_csu_fini> 556: 48 8d 0d 63 02 00 00 lea 0x263(%rip),%rcx # 7c0 <__libc_csu_ini 5b4 t> 55d: 48 8d 3d 2c 02 00 00 lea 0x22c(%rip),%rdi # 790 <main> 564: ff 15 76 0a 20 00 callq *0x200a76(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5> 56a: f4 hlt 56b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
跟据上述汇编,其实也就做了一件事,调用
__libc_start_main函数,并向其传递了7个参数:
- r9传递 rdx
- r8传递
__libc_csu_fini
- rcx传递
__libc_csu_init
- rdx传递 argv
- rsi传递 argc
- rdi传递 main
- 栈传递 rsp的值
上述汇编有几句比较晦涩:
- and $0xfffffffffffffff0,%rsp的目的是使rsp对齐到16字节。
- push %rax 为了在调用
__libc_start_main
之前,帮助rsp对齐到16字节,%rax入栈无其它意义。显然,这一句执行后,rsp还没有对齐到16字节,下一句汇编执行后就将对齐到16字节。 - push %rsp, rsp的值入栈,这时将rsp的值传递给
__libc_start_main
函数,且使rsp对齐到16字节。
执行_start的第一条指令时,rsp的值是多少呢?谁设置的呢?rsp的值是bprm->p,Linux内核设置的,在上面的内容中有介绍。下图结合了Linux Kernel和_start设置的栈。其实
_start来自glibc,在x86-64平台上,可以在文件
sysdeps/x86_64/start.S中找到代码。这段代码的目的很单纯,只是给函数
__libc_start_main准备参数。函数
__libc_start_main同样来自glibc,它定义在文件csu/libc-start.c中。
![](http://i2.51cto.com/images/blog/201808/07/a1762db5c8d95005dd9d8c4dfedf78bc.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
函数
__libc_start_main的原型如下:
int __libc_start_main( (int (*main) (int, char**, char**), int argc, char **argv, __typeof (main) init, void (*fini) (void), void (*rtld_fini) (void), void* stack_end)
在《How statically linked programs run on Linux 》中介绍了
__libc_start_main的作用:
- Figure out where the environment variables are on the stack.
- Prepare the auxiliary vector, if required.
- Initialize thread-specific functionality (pthreads, TLS, etc.)
- Perform some security-related bookkeeping (this is not really a separate step, but is trickled all through the function).
- Initialize libc itself.
- Call the program initialization function through the passed pointer (init).
- Register the program finalization function (fini) for execution on exit.
- Call main(argc, argv, envp)
- Call exit with the result of main as the exit code.
2、ARM64
2.1 工具链
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt install gcc-arm-linux-gnueabi
2.2、 adr,ldr,adrp 指令
2.2.1 adr
主要用于形成pc相对地址,把相对地址load到寄存器中,使用方法为:
adr <xd>, <label>
当前指令到label的偏移 offset_to_label 加上PC的值,然后将结果赋值给xd。offset_to_label可以是个负数,实际在执行过程中会将offse_to_label扩展成64为有符号数。但是ARM指令的长度是固定为32bit,offset_to_label最多只能为21位,也即可以寻PC +/-1MB的范围。
经常会被编译器转换成add或sub指令:
add <xd>,[PC, #offset_to_label] sub <xd>,[PC, #-offset_to_label]
2.2.2 ldr
这个指令的本质作用是把地址中的数据加载到寄存器中,根据地址的表达形式不同可以分为几种情况:
ldr <xd>, <label>
将程序label处的数据load到xd中,label是一个地址。指令记录的不是label的绝对地址,是当前指令到label的偏移,记作offset_to_labe,l和adr指令描述中的 offset_to_label 有所不同。在汇编时,汇编器会计算当前指令到label的偏移量(以字节为单位),然后将偏移量右移两位得到 offset_to_label 。在执行执行指令时效果如下:
xd < [PC + (offset_to_label << 2)]
另外几种如下:
ldr <Xt>,[<Xn|SP>],#<simm> post_index ldr <Xt>,[<Xn|SP>,#<simm>]! pre_index ldr <Xt>,[<Xn|SP>,#<pimm>] unsigned_offset
2.2.3 adrp
该指令在ARMv8中首次被设计出来,是ARM指令集的一个重大创新,可以减少指令条数以及访存的次数。有几篇博客介绍了该指令的作用,但是没有讲清楚,如《ARM指令浅析2(adrp、b)》、《汇编七、ADRP指令》。
指令的使用方式为:
adrp <Xt>, <label>
adrp就是address page 的简写,这里的page指的是大小为4KB的连续内存,和操作系统中的页不是一回事。该指令的作用是将label所在页相对于当前指令所在页的页偏移数目放进Xt中。
2.3、_start 函数
$aarch64-linux-gnu-objdump -d test 0000000000000620 <_start>: 620: d280001d mov x29, #0x0 // #0 624: d280001e mov x30, #0x0 // #0 628: aa0003e5 mov x5, x0 62c: f94003e1 ldr x1, [sp] 630: 910023e2 add x2, sp, #0x8 634: 910003e6 mov x6, sp 638: 90000080 adrp x0, 10000 <__FRAME_END__+0xf66c> 63c: f947f800 ldr x0, [x0, #4080] 640: 90000083 adrp x3, 10000 <__FRAME_END__+0xf66c> 644: f947f463 ldr x3, [x3, #4072] 648: 90000084 adrp x4, 10000 <__FRAME_END__+0xf66c> 64c: f947e484 ldr x4, [x4, #4040] 650: 97ffffe4 bl 5e0 <__libc_start_main@plt> 654: 97ffffeb bl 600 <abort@plt>
- X86-64和ARM64用户栈的结构 (4) --- mian()函数和子函数之间的栈
- 搭建Android x86_64及arm64-v8a操作步骤
- 在fedora9 x86_64上编译arm qt2.2.0
- ( OK ) CentOS 7 + android-ndk-r10d-linux-x86_64 + Android (ARM)—ndk-build
- Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题
- 【技术】【操作】制作同时支持armv7,armv7s,arm64,i386,x86_64的静态库.a
- 为x86 64体系结构添加系统调用
- 使用PXE+DHCP+Apache+Kickstart无人值守安装CentOS5.8 x86_64
- iOS开发~静态库.a中的armv7,armv7s,arm64,i386,x86_64含义
- Xcode8.3静态库libmp3lame.a无法真机运行!lame静态库libmp3lame.a 编译,支持arm64 armv7s x86_64 i386 armv7
- IOS:armv7,armv7s,arm64,i386,x86
- Windows7中搭建Android x86_64及armv8-a操作步骤
- Android 中arm64-v8a、armeabi-v7a、armeabi、x86简介~
- 常见函数调用约定(x86、x64、arm、arm64)
- 我的Android进阶之旅------>Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题
- Undefined symbols for architecture x86_64: "_SDL_main", referenced from:报错
- xcode6 编译支持 i386 x86_64 arm7 arm7v arm64版本opencore-amr库
- 使用PXE+DHCP+Apache+Kickstart无人值守安装CentOS5.8 x86_64
- ios 中armv7,armv7s,arm64,i386,x86_64是什么意思
- Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题