|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
最近,看了很多关于uboot的分析,其中就有说要为C语言的运行,就要准备好堆栈。' G$ g* _; f4 p6 l1 l. N9 M0 G
& y) K2 j: e) W: r5 F% `$ Y
1 E; c! b; _; @: g; Y5 a5 |2 D0 A: d, [+ y
2 }9 S5 _; E3 n& l9 H) v* L, z而在Uboot的start.S汇编代码中,关于系统初始化,也看到有堆栈指针初始化这个动作。但是,从来只是看到有人说系统初始化要初始化堆栈,即正确给堆栈指针sp赋值,但是却从来没有看到有人解释,为何要初始化堆栈。今天,我们就来试图解释一下,为何要初始化堆栈,即:3 V/ T* b: \: D9 R$ m) }# _# s
1 e2 k' z& |( `, n7 l' ]+ \; H# h: m/ {/ M) o! ?
) g1 F# B4 P) E
: ^% Z: K& F8 K2 ]( t6 _5 U$ M, M为何C语言的函数调用要用到堆栈,而汇编却不需要初始化堆栈?
: B9 o* F% }7 u- z) k5 s7 _& }1 S& [5 d# U) w6 N% K2 G( P, v% ]8 t
6 C) n ^" v% H7 P; a& a* K6 ~+ l% i4 g5 |! n, u- ]* M
: a# e. A2 w0 e7 ~要明白这个问题,首先要了解堆栈的作用。. o: _: \$ g& Z L
: [" c1 e% q& t* ^8 B% d( I; g5 F: T
+ e" n4 m/ C4 ]; D, ~. J% v. p2 {$ @# k2 s, A4 i
关于堆栈的作用,要详细讲解的话,要很长的篇幅,所以此处只是做简略介绍。
& D) g4 T" G; j1 R9 `* j! Y) J5 h, s3 }9 S# h$ j2 t( T
总的来说,堆栈的作用就是:保存现场/上下文,传递参数。
6 |8 a; j' |8 {! p+ c& q0 P2 U% X5 a, ^" s# [
4 W9 `" O, P4 X& K9 u1 Q3 }2 Y2 H5 x5 x" u. f) Q% {( U& |
1.保存现场/上下文- o- _; `1 ?- i$ T! b% ` |* l( d
现场,意思就相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢复现场了。而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如r0,r1等等,对于这些寄存器的值,如果你不保存而直接跳转到子函数中去执行,那么很可能就被其破坏了,因为其函数执行也要用到这些寄存器。( P$ W3 y& d9 }& e$ y0 f1 q& ~; e, J9 Y
& `! A% @4 w |# z: \因此,在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场。这样CPU就可以正确的继续执行了。
+ q5 Q/ e' @: f; b9 ^4 t$ ^( s9 J3 ?; M) s3 W
在计算机中,你常可以看到上下文这个词,对应的英文是context。那么:% S @+ x5 N: N8 ]7 P
/ _4 O' A( o$ F
1.1.什么叫做上下文context
( q7 A: ]1 H3 U, V+ X保存现场,也叫保存上下文。4 P/ A; x$ H( V2 u+ P! g' A) J8 I( b& h
+ o2 J" c1 X0 J2 I5 Z
; Y' d9 ]' q7 E0 ~) H" @% I7 f9 `6 [+ p7 x! b' H' S
上下文,英文叫做context,就是上面的文章,和下面的文章,即与你此刻,当前CPU运行有关系的内容,即那些你用到寄存器。所以,和上面的现场,是一个意思。. v8 ~' r- H; C
) F1 m: Y- Q T! X' V$ `0 l; a9 q8 t- e P: o! Z
保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到堆栈中,把对应的值压入到堆栈里面,即所谓的压栈。' g7 M; ~' W' J$ N$ b$ Z; j# g6 z5 A+ M2 Y6 z. Y3 b4 Z) x$ m
9 B& e/ C; |: O# Y3 G# Q4 Y. ~/ T N5 w4 O( C$ ]- ^) i5 p% f5 Z
8 u$ A1 {$ q: ]& P! p% z4 f, p/ `
然后待被调用的子函数执行完毕的时候,再调用pop,把堆栈中的一个个的值,赋值给对应的那些你刚开始压栈时用到的寄存器,把对应的值从堆栈中弹出去,即所谓的出栈。
( b# Z% I7 a% I; X& g& I o6 m2 w) V- H& l# F2 [
其中保存的寄存器中,也包括lr的值(因为用bl指令进行跳转的话,那么之前的pc的值是存在lr中的),然后在子程序执行完毕的时候,再把堆栈中的lr的值pop出来,赋值给pc,这样就实现了子函数的正确的返回。; k6 s! F- y1 D* h2 N3 o3 A: A* z7 n
+ W# a, o: d* }8 v+ o' X! z; x7 P- s+ t( @3 j' P6 Z
2.传递参数- v: P* A9 {( r3 W8 U1 k- }* X4 L" C' F5 P$ A. X* h1 S, v" d7 i
C语言进行函数调用的时候,常常会传递给被调用的函数一些参数,对于这些C语言级别的参数,被编译器翻译成汇编语言的时候,就要找个地方存放一下,并且让被调用的函数能够访问,否则就没发实现传递参数了。对于找个地方放一下,分两种情况。2 v0 t" @# h3 C" _" c* y: _
3 X1 z% g1 ?$ _$ W3 Y! C/ X* C$ F9 ^, D$ K' g1 ~$ H( S# U' i$ \4 l/ a, G
一种情况是,本身传递的参数就很少,就可以通过寄存器传送参数。
* A$ X& ]6 t- L$ u: G0 {9 O; |4 D1 V- J/ a9 L+ g+ m) V- p! b
因为在前面的保存现场的动作中,已经保存好了对应的寄存器的值,那么此时,这些寄存器就是空闲的,可以供我们使用的了,那就可以放参数,而参数少的情况下,就足够存放参数了,比如参数有2个,那么就用r0和r1存放即可。(关于参数1和参数2,具体哪个放在r0,哪个放在r1,就是和APCS中的“在函数调用之间传递/返回参数”相关了,APCS中会有详细的约定。感兴趣的自己去研究。); i! J& m" m; w. l; y: W9 J
8 Y. i" `- F+ ]7 f: {- g- z: f1 o$ f* i
但是如果参数太多,寄存器不够用,那么就得把多余的参数堆栈中了。8 u, a! w7 c4 N# f' |" S' Z% Z5 ]& O/ F8 s# j3 ]
, Y$ j/ f" b" |+ T6 C3 ^0 N
即,可以用堆栈来传递所有的或寄存器放不下的那些多余的参数。
; B! a1 p2 Y3 ^9 E5 l: M" T4 `4 p3 o3 Z! s8 e$ W& y. z% s! @/ ?) q6 g8 S& o5 p, l
3.举例分析C语言函数调用是如何使用堆栈的! z* V' k! G, p( y, ?2 E: w) N$ Z" N- {/ d l* P
对于上面的解释的堆栈的作用显得有些抽象,此处再用例子来简单说明一下,就容易明白了:
. Y+ q8 P4 |4 ?8 a( d+ F6 L. t0 U) o3 S7 Z
" @6 ]6 k1 _* ]' u5 @+ b1 c& E
用:
: r/ H! h6 i+ y7 @/ i9 N4 g/ q- f4 k9 Q1 a3 e/ g5 ^) Q1 Z" ` M
! b: w1 Y1 M& O. n
1. ARM-inux-objdump –d u-boot > dump_u-boot.txt% H+ y' t. O. X. N4 g( N. l W% B- [4 `* N
7 T: T4 j7 h7 ]: Z' N/ o% W C+ y4 B
" X9 ?4 B2 s6 M5 {' S& n
' `; m2 n" F7 d7 X- x! R+ ]. v可以得到dump_u-boot.txt文件。该文件就是中,包含了u-boot中的程序的可执行的汇编代码,其中我们可以看到C语言的函数的源代码,到底对应着那些汇编代码。0 ~* S3 D7 L* Z: V" H" \% c" {
5 G2 p& k: H8 v" \! U: v2 W1 l1 y& L# c/ w
1 S6 @; t- y! d' J8 N" G( C下面贴出两个函数的汇编代码,7 l/ U7 r3 R; J U+ b8 \$ u3 p/ [
3 ?4 L( }! Y) l" Y6 q' V: \- w$ [一个是clock_init,: p3 w) J# R+ |' @) Y
另一个是与clock_init在同一C源文件中的,另外一个函数CopyCode2Ram:; n; n/ f+ d! @' C& G" Q7 R- k1 ]/ n& b+ j A& }2 I' p
8 _% f4 N. S) U1. 33d0091c <CopyCode2Ram>:
% c# a0 j z# ]+ ^1 O2. 33d0091c: e92d4070 push {r4, r5, r6, lr}
; d! b' S$ j8 G3. 33d00920: e1a06000 mov r6, r08 G9 Y* q, p( ~% Y
4. 33d00924: e1a05001 mov r5, r1
1 X6 o+ p Z1 x# g5 a5. 33d00928: e1a04002 mov r4, r2
, l2 B6 B# m- {8 _- f1 h$ _6. 33d0092c: ebffffef bl 33d008f0 <bBootFrmNORFlash>. K. s6 B2 Q3 T
7. ... ...
7 V7 Y a" a% l0 S3 s* |8. 33d00984: ebffff14 bl 33d005dc <nand_read_ll>. e! F3 s5 r) E Z1 W$ b1 W0 A* p2 `9 L) d( X' L) g# [) C3 f
9. ... ...) x6 ]3 K: {+ J1 f) @3 q! v9 s2 \( Z1 I! S) V$ y& F- T
10. 33d009a8: e3a00000 mov r0, #0 ; 0x0% C5 a1 o0 R9 \; O% Z
+ U" s, n* }: S& A }- E' e11. 33d009ac: e8bd8070 pop {r4, r5, r6, pc}# B' E, C4 b5 u& L! [: ^
7 k2 p) `; r4 P8 ~; p7 X7 m* \3 E9 }0 f12.
( W5 A' j! \0 R5 ]. v! b- \$ l13. 33d009b0 <clock_init>:. }! u* w2 R7 V9 W3 ~7 R* D k: C; p* A1 D
14. 33d009b0: e3a02313 mov r2, #1275068416 ; 0x4c000000+ y1 H2 a- W6 y4 F; U& c) F
9 f$ D& ^0 n: d" O! r15. 33d009b4: e3a03005 mov r3, #5 ; 0x5
7 S: ~/ P- I9 A A9 [/ N# S16. 33d009b8: e5823014 str r3, [r2, #20]4 ?) Z! L1 ]( K! Z$ F4 l
+ v4 S' T2 Z/ \. `; A( W9 s1 C17. ... ...4 v. J% Y1 v# ?+ @4 P# n2 V; [5 Z( h7 u/ K. U, f& h
18. 33d009f8: e1a0f00e mov pc, lr
5 i& W4 f9 U/ H! @% y/ W% ^( Q o3 {
(1)clock_init部分的代码
+ K( W+ l& ]/ R/ _可以看到该函数第一行:4 i; W! N1 s$ F9 F8 ?) I7 j& u3 d; ~
: D) z( e6 L8 U( ^
) a: z k. b+ S; O3 i4 G9 B4 k, l2 m1. 33d009b0: e3a02313 mov r2, #1275068416 ; 0x4c0000002 n% a. n% g+ c! L
: i" E- M. o: u* b! s7 D/ W1 C1 M2 T" C
就没有我们所期望的push指令,没有去将一些寄存器的值放到堆栈中。这是因为,我们clock_init这部分的内容,所用到的r2,r3等等寄存器,和前面调用clock_init之前所用到的寄存器r0,没有冲突,所以此处可以不用push去保存这类寄存器的值,不过有个寄存器要注意,那就是r14,即lr,其是在前面调用clock_init的时候,用的是bl指令,所以会自动把跳转时候的pc的值赋值给lr,所以也不需要push指令去将PC的值保存到堆栈中。 f. J) m& M8 g. m6 B$ T
) b, _1 o! e, Z9 O% F, X
8 ?; H1 S: f% y3 m
: w5 e% M& _; x2 U而clock_init的代码的最后一行:& l0 W, [: Q5 `
- Y' d. x, U/ m% P0 _7 G5 q; |* ^7 b6 c' Y% u9 U' g Y+ ^! S4 p0 U
1. 33d009f8: e1a0f00e mov pc, lr
( `- B' k' Q) W2 _% }) \$ [7 r+ t8 [+ \7 m B t" q( z. v/ ^; v) |* b
9 M9 e$ C0 Q) Z6 I6 z x t$ h6 z
就是我们常见的mov pc, lr,把lr的值,即之前保存的函数调用时候的PC值,赋值给现在的PC,这样就实现了函数的正确的返回,即返回到了函数调用时候下一个指令的位置。2 P8 O# [3 {% ^ u8 ~/ g( _; I; {0 F8 B# `' r6 W% r
4 k4 I. z F- R( G& T
& q. M" _" R Z3 [) r5 T) B( U) s2 q
这样CPU就可以继续执行原先函数内剩下那部分的代码了。; x: b! [" s; Z+ Y7 y) F
! O& i8 t& E$ Y9 |# y" G/ k9 G; _; Y0 A4 B0 e+ x0 Y @3 f5 x: m h$ T( N; j# g% m1 m/ E
+ M: r4 l* Y2 J
; Y0 K( C* ]% d0 f(2)CopyCode2Ram部分的代码
, K$ E9 S7 x) |/ g# R& |' S. Z: G' g7 F* _% Q( V: X8 l% P i! H; ~+ d
1. 33d0091c: e92d4070 push {r4, r5, r6, lr}
% Z, j% N7 ~4 s: m1 t2 f' ?2 h1 n3 [, J4 B5 F8 {8 \, y3 _/ h( t( M# R7 {
) z# O! s+ h% J6 o4 s7 r( a就是我们所期望的,用push指令,保存了r4,r5,r以及lr。用push去保存r4,r5,r6,那是因为所谓的保存现场,以后后续函数返回时候再恢复现场,而用push去保存lr,那是因为此函数里面,还有其他函数调用:# F" K8 u8 `" H1 K$ ^6 R6 z" {
' O+ O% u0 O7 l) f4 q0 E3 [
7 P0 B& d ^5 n. j' ~2 B4 e
1. 33d0092c: ebffffef bl 33d008f0 <bBootFrmNORFlash>" v; ~$ Q) }; z" I5 `
$ L" D; z" G/ [+ n' |2. ... ...
) o% S4 A& T! [* q D# j! R0 f7 p3. 33d00984: ebffff14 bl 33d005dc <nand_read_ll>* A5 K1 ?" K$ y" r' a
8 c* r& x4 `6 O. u1 |4. ... ...% W+ ?, d g. h( [0 B3 i: z t) ^/ m1 T: o+ L% g8 D
; R: L) s- B3 j- x$ |8 B) n
4 I& I' y8 Q+ m# O( k也用到了bl指令,会改变我们最开始进入clock_init时候的lr的值,所以我们要用push也暂时保存起来。而对应地,CopyCode2Ram的最后一行:7 ]. |% g, C. c
# e. L6 @) J s# V6 b! `( j
) z' m9 o* H' }. E! a7 Y4 \+ k4 j" @. `
1. 33d009ac: e8bd8070 pop {r4, r5, r6, pc}# v5 \; H: s3 h" |' ~) f) {0 k
0 |! G0 m; R5 ~6 J G/ X
- Z u. a9 m5 X+ }9 Y0 b$ K& h- P( c
就是把之前push的值,给pop出来,还给对应的寄存器,其中最后一个是将开始push的lr的值,pop出来给赋给PC,因为实现了函数的返回。另外,我们注意到,在CopyCode2Ram的倒数第二行是:' }+ N) `( [5 x3 R* N
! E) n; Y# q& D) A% O
% H8 g; a9 J8 q/ u9 N8 C- Z1 |8 I) [* n# ~! {, f" K8 |# M+ n
1. 33d009a8: e3a00000 mov r0, #0 ; 0x05 @/ b+ n7 J" U. B& |7 `# b5 g6 [4 F
! A" m; u" c7 W- j: w) X: B; ~" p8 I/ D% i
是把0赋值给r0寄存器,这个就是我们所谓返回值的传递,是通过r0寄存器的。$ _, r# J$ z6 Y- H1 t1 [9 J
% v# w3 c# v, R; S1 l8 `8 A( A$ ~: h8 m! ?) S ?! [% I
9 X8 O, r, ~2 _ Y此处的返回值是0,也对应着C语言的源码中的“return 0”.
) f: t' Y/ _) i* H, A& b% L$ ~
7 _2 Z" B' m1 y' `' k对于使用哪个寄存器来传递返回值:
, n, k! t) b, I& u3 f5 ], ^当然你也可以用其他暂时空闲没有用到的寄存器来传递返回值,但是这些处理方式,本身是根据ARM的APCS的寄存器的使用的约定而设计的,你最好不要随便改变使用方式,最好还是按照其约定的来处理,这样程序更加符合规范。
" O' M) Z0 y% |& m |
|