m18317710169

gdb+gdbserver的远程调试笔记


创建时间:2017/3/9 15:37更新时间:2017/3/10 10:37作者:Robbie Jun


前言:

某些时候由于模拟环境的限制,调试必须要在目标板上进行。由于嵌入式系统资源比较有限,一般不能在目标板上直接构建GDB的调试环境,

这时我们通常采用gdb+gdbserver的远程调试方法:gdbserver在目标板中运行,而gdb则在主机上运行。


调试准备:

1.目标板与主机在同一个局域网:互相可以ping通

2.假设目标板的ip是:192.168.199.155  假设主机的ip是:192.168.199.118

3.交叉编译gdbserver并拷贝到目标板

4.交叉调试器arm-linux-gnueabihf-gdb可执行


运行应用程序:

gdbserver 192.168.199.118:5000 ./netflix    # 192.168.199.118主机ip;5000监听的端口;netflix应用程序名称

运行上面的应用程序打印如下:

Process ./netflix created; pid = 5018

Listening on port 5000


主机端执行:

arm-linux-gdb ./netflix    # arm-linux-gdb为gdb调试工具(名称不唯一);netflix应用程序名称,和上面是同一个bin

运行上面的程序后打印如下:

GNU gdb (crosstool-NG 1.20.0) 7.8

Copyright (C) 2014 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

and "show warranty" for details.

This GDB was configured as "--host=x86_64-build_unknown-linux-gnu --target=arm-unknown-linux-gnueabi".

Type "show configuration" for configuration details.

For bug reporting instructions, please see:

<http://www.gnu.org/software/gdb/bugs/>.

Find the GDB manual and other documentation resources online at:

<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".

Type "apropos word" to search for commands related to "word"...

Reading symbols from ./netflix...(no debugging symbols found)...done.

(gdb)


在(gdb)命令行执行如下:

target remote 192.168.199.155:5000   # 192.168.199.155为目标板ip

执行上面的指令后打印如下:

Remote debugging using 192.168.199.155:5000

warning: Unable to find dynamic linker breakpoint function.

GDB will be unable to debug shared library initializers

and track explicitly loaded dynamic code.

0xf70b2a40 in ?? ()

(gdb)


开始调试:

一般来说,GDB主要帮忙你完成下面四个方面的功能

1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。

2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)

3、当程序被停住时,可以检查此时你的程序中所发生的事。

4、动态的改变你程序的执行环境。


 调试源码如下:

 root@vm:/home/xurenjun/share/debug# ls

 func.c  main.c  test


 func.c :

  1 #include <string.h>

  2 #include <stdlib.h>       

  3 #include <stdio.h>

  4

  5 void print2(int size,char* string){

  6     char* ptr;           

  7     ptr=(char*)malloc(size);

  8     memset(ptr,'\0',size);

  9     strcpy(ptr,string);

 10     printf("[%s] %s\n",__FUNCTION__,ptr);

 11     free(ptr);

 12     ptr=(void*)0;

 13 }


main.c :

  1 #include <stdio.h>

  2 #include <stdlib.h>       

  3 #include <string.h>

  4

  5 void print2(int,char*);   

  6   

  7 void print1(int* val){   

  8     printf("[%s] val = %d\n",__FUNCTION__,*val);

  9 }

 10

 11 int main(void){

 12     int i = 0;

 13     int res = 0;

 14     for(i=0; i<=100; i++){

 15         res += i;

 16     }

 17     print1(&res);

 18     print2(100,"this is test demo!");

 19     return 0;

 20 }


编译:

arm-linux-gnueabihf-gcc  -g -c main.c -o main.o

arm-linux-gnueabihf-gcc  -g -c func.c -o func.o

arm-linux-gnueabihf-gcc  -g main.o func.o -o test

注意此处编译带-g选项是为了产生调试的符号信息否则调试的时候会出现如下错误:No symbol table is loaded.  Use the "file" command.


GDB调试常用命令:

list(l)                               查看程序

break(b)  函数名             在某函数入口 加断点

run                                  开始执行程序

break(b)  行号                在指定行添加断点

break(b)  文件名:行号     在指定文件的指定行号加断点

info break                      查看所有设置的断 点

delete 某断点编号           删除某断点

next(n)                            单步运行程序(不进入子函数)

step(s)                             单步运行程序(进入子函数)

continue(c)                     继续运行程序

watch  变量名                   监视某一个变量

backtrace                       命令可以在遇到断点而暂停执行时显示栈帧。此外,backtrace 的别名还有 where 和 info stack

print(p)    变量名            查看指定变量值

set var=value                 设置变量的值

quit(q)                            退出gdb


针对上面的源码开始调试实验:

使用list或者l查看源码,enter键进入下一页:

(gdb) l ist

3       #include <string.h>

4

5       void print2(int,char*);

6

7       void print1(int* val){

8               printf("[%s] val = %d\n",__FUNCTION__,*val);

9       }

10

11      int main(void){

12              int i = 0;

(gdb)

13              int res = 0;

14              for(i=0; i<=100; i++){

15                      res += i;

16              }

17              print1(&res);

18              print2(100,"this is test demo!");

19              return 0;

20      }

(gdb)


使用list + 行号 表示显示从指定行号的位置为中心开始上下显示10行,enter键进入下一页 

(gdb) list 13

8               printf("[%s] val = %d\n",__FUNCTION__,*val);

9       }

10

11      int main(void){

12              int i = 0;

13              int res = 0;

14              for(i=0; i<=100; i++){

15                      res += i;

16              }

17              print1(&res);

(gdb)


使用break + 函数名 表示在函数名处设置断点:

(gdb) break print2

Breakpoint 1 at 0x4006ec: file ./func.c, line 7.

(gdb)


使用info break 查看设置的所有断点:

(gdb) info break

Num     Type           Disp Enb Address            What

1       breakpoint     keep y   0x00000000004006ec in print2 at ./func.c:7

        breakpoint already hit 1 time

2       breakpoint     keep y   <PENDING>          info

3       breakpoint     keep y   <PENDING>          info 


使用run即开始运行,遇到断点停止运行:

(gdb) break print2

Breakpoint 1 at 0x4006ec: file ./func.c, line 7.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


[print1] val = 5050


Breakpoint 1, print2 (size=100, string=0x4007f3 "this is test demo!") at ./func.c:7

7               ptr=(char*)malloc(size);

(gdb)


使用next可以单步运行程序,如果不是在子程序中设置断点,那么next不会进入子程序(下面是进入子程序的情况):

(gdb) break print2

Breakpoint 1 at 0x4006ec: file ./func.c, line 7.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


[print1] val = 5050


Breakpoint 1, print2 (size=100, string=0x4007f3 "this is test demo!") at ./func.c:7

7               ptr=(char*)malloc(size);

(gdb)

(gdb) next

8               memset(ptr,'\0',size);

(gdb) next

9               strcpy(ptr,string);

(gdb)


使用break(b)  文件名:行号在某个文件的某行打上断点;如在main.c的第8行打一个断点:

(gdb) break  main.c:8   

Breakpoint 1 at 0x40064c: file main.c, line 8.


使用delete 某断点编号 ;删除某个断点:

(gdb) info break

Num     Type           Disp Enb Address            What

1       breakpoint     keep y   0x000000000040064c main.c:8

        breakpoint already hit 1 time

2       breakpoint     keep y   <PENDING>          info

(gdb) delete 1

(gdb) info break

Num     Type           Disp Enb Address    What

2       breakpoint     keep y   <PENDING>  info

(gdb) delete 2

(gdb) info break

No breakpoints or watchpoints.


使用step可以单步运行程序,会进入子程序:

(gdb) break 9

Breakpoint 1 at 0x400668: file main.c, line 9.

(gdb) break 17

Breakpoint 2 at 0x40069e: file main.c, line 17.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 2, 0x000000000040069e in main ()

(gdb) step

Single stepping until exit from function main,

which has no line number information.

[print1] val = 5050


Breakpoint 1, 0x0000000000400668 in print1 ()

(gdb)

Single stepping until exit from function print1,

which has no line number information.

0x00000000004006aa in main ()

(gdb)

Single stepping until exit from function main,

which has no line number information.

[print2] this is test demo!

__libc_start_main (main=0x40066a <main>, argc=1, ubp_av=0x7fffffffe578, init=<optimized out>, fini=<optimized out>,

    rtld_fini=<optimized out>, stack_end=0x7fffffffe568) at libc-start.c:258

258     libc-start.c: No such file or directory.


使用continue(c)继续运行程序(为了方便观看main程序多加一句打印)

continue 命令继续运行程序。程序会在遇到断点后再次暂停运行。如果没有遇到断点,就会一直运行到结束。

(gdb) continue

(gdb) continue 次数:

main.c :

1 #include <stdio.h>

2 #include <stdlib.h>       

3 #include <string.h>

4

5 void print2(int,char*);   

6   

7 void print1(int* val){   

8     printf("[%s] val = %d\n",__FUNCTION__,*val);

9 }

10

11 int main(void){

12     int i = 0;

13     int res = 0;

14     for(i=0; i<=100; i++){

15         res += i;

16         printf("res=%d\n",res);       

17     }

18     print1(&res);

19     print2(100,"this is test demo!");

20     return 0;

21 }


(gdb) break 16

Breakpoint 1 at 0x400694: file main.c, line 16.

(gdb) break 20

Breakpoint 2 at 0x4006cd: file main.c, line 20.

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 1, 0x0000000000400694 in main ()

(gdb) next

Single stepping until exit from function main,

which has no line number information.

res=0


Breakpoint 1, 0x0000000000400694 in main ()

(gdb)

Single stepping until exit from function main,

which has no line number information.

res=1


Breakpoint 1, 0x0000000000400694 in main ()

(gdb)

Single stepping until exit from function main,

which has no line number information.

res=3


Breakpoint 1, 0x0000000000400694 in main ()

(gdb) continue

Continuing.

res=6


Breakpoint 1, 0x0000000000400694 in main ()

(gdb) continue 100

Will ignore next 99 crossings of breakpoint 1.  Continuing.

res=10

res=15

res=21

res=28

res=36

res=45

res=55

res=66

res=78

res=91

res=105

res=120

res=136

res=153

res=171

res=190

res=210

res=231

res=253

res=276

res=300

res=325

res=351

res=378

res=406

res=435

res=465

res=496

res=528

res=561

res=595

res=630

res=666

res=703

res=741

res=780

res=820

res=861

res=903

res=946

res=990

res=1035

res=1081

res=1128

res=1176

res=1225

res=1275

res=1326

res=1378

res=1431

res=1485

res=1540

res=1596

res=1653

res=1711

res=1770

res=1830

res=1891

res=1953

res=2016

res=2080

res=2145

res=2211

res=2278

res=2346

res=2415

res=2485

res=2556

res=2628

res=2701

res=2775

res=2850

res=2926

res=3003

res=3081

res=3160

res=3240

res=3321

res=3403

res=3486

res=3570

res=3655

res=3741

res=3828

res=3916

res=4005

res=4095

res=4186

res=4278

res=4371

res=4465

res=4560

res=4656

res=4753

res=4851

res=4950

res=5050

[print1] val = 5050

[print2] this is test demo!


Breakpoint 2, 0x00000000004006cd in main ()   //再次遇到断点


print 变量名;查看变量的值:

main.c添加变量mval:


1 #include <stdio.h>

2 #include <stdlib.h>       

3 #include <string.h>

4

5 int mval;                 

6                           

7 void print2(int,char*);   

8   

9 void print1(int* val){   

10     printf("[%s] val = %d\n",__FUNCTION__,*val);

11 }

12

13 int main(void){

14     int i = 0;

15     int res = 0;

16     mval=100;

17     for(i=0; i<=100; i++){

18         res += i;

19         printf("res=%d\n",res);       

20     }

21     print1(&res);

22     print2(100,"this is test demo!");

23     return 0;

24 }


(gdb) break 14

Breakpoint 1 at 0x400672: file main.c, line 14.

(gdb) break 17

Breakpoint 2 at 0x40068a: main.c:17. (2 locations)

(gdb) print mval

$1 = 0

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 1, 0x0000000000400672 in main ()

(gdb) print mval

$2 = 0

(gdb) next

Single stepping until exit from function main,

which has no line number information.


Breakpoint 2, 0x000000000040068a in main ()

(gdb) print mval

$3 = 100


watch  变量名;监视某一个变量:

(gdb) watch mval

Hardware watchpoint 1: mval

(gdb) run

Starting program: /home/xurenjun/share/debug/test

Hardware watchpoint 1: mval


Old value = 0

New value = 100

0x000000000040068a in main ()


backtrace 命令可以在遇到断点而暂停执行时显示栈帧。此外,backtrace 的别名还有 where 和 info stack

(gdb) break 17

Breakpoint 1 at 0x40068a: main.c:17. (2 locations)

(gdb) run

Starting program: /home/xurenjun/share/debug/test


Breakpoint 1, 0x000000000040068a in main ()

(gdb) backtrace

#0  0x000000000040068a in main ()

(gdb)







常见的0欧姆电阻与磁珠的作用


1,在电路中没有任何功能,只是在PCB上为了调试方便或兼容设计等原因。
2,可以做跳线用,如果某段线路不用,直接不贴该电阻即可(不影响外观)
3,在匹配电路参数不确定的时候,以0欧姆代替,实际调试的时候,确定参数,再以具体数值的元件代替。
4,想测某部分电路的耗电流的时候,可以去掉0ohm电阻,接上电流表,这样方便测耗电流。
5,在布线时,如果实在布不过去了,也可以加一个0欧的电阻
6,在高频信号下,充当电感或电容。(与外部电路特性有关)电感用,主要是解决EMC问题。如地与地,电源和IC Pin间
7,单点接地(指保护接地、工作接地、直流接地在设备上相互分开,各自成为独立系统。)
8,熔丝作用

磁珠
电感是储能元件,而磁珠是能量转换(消耗)器件。电感多用于电源滤波回路,侧重于抑止传导性干扰;磁珠多用于信号回路,主要用于EMI方面。磁珠用来吸收超高频信号,象一些RF电路,PLL,振荡电路,含超高频存储器电路(DDR,SDRAM,RAMBUS等)都需要在电源输入部分加磁珠,而电感是一种储能元件,用在LC振荡电路、中低频的滤波电路等,其应用频率范围很少超过50MHz。磁珠有很高的电阻率和磁导率,他等效于电阻和电感串联,但电阻值和电感值都随频率变化。 他比普通的电感有更好的高频滤波特性,在高频时呈现阻性,所以能在相当宽的频率范围内保持较高的阻抗,从而提高调频滤波效果。 
作为电源滤波,可以使用电感。磁珠的电路符号就是电感但是型号上可以看出使用的是磁珠在电路功能上,磁珠和电感是原理相同的,只是频率特性不同罢了


*模拟地和数字地单点接地*
  只要是地,最终都要接到一起,然后入大地。如果不接在一起就是"浮地",存在压差,容易积累电荷,造成静电。地是参考0电位,所有电压都是参考地得出的,地的标准要一致,故各种地应短接在一起。人们认为大地能够吸收所有电荷,始终维持稳定,是最终的地参考点。虽然有些板子没有接大地,但发电厂是接大地的,板子上的电源最终还是会返回发电厂入地。如果把模拟地和数字地大面积直接相连,会导致互相干扰。不短接又不妥,理由如上有四种方法解决此问题:1、用磁珠连接;2、用电容连接;3、用电感连接;4、用0欧姆电阻连接。
  磁珠的等效电路相当于带阻限波器,只对某个频点的噪声有显著抑制作用,使用时需要预先估计噪点频率,以便选用适当型号。对于频率不确定或无法预知的情况,磁珠不合。 
  电容隔直通交,造成浮地。
  电感体积大,杂散参数多,不稳定。


  0欧电阻相当于很窄的电流通路,能够有效地限制环路电流,使噪声得到抑制。电阻在所有频带上都有衰减作用(0欧电阻也有阻抗),这点比磁珠强。

0欧电阻
  *跨接时用于电流回路*
  当分割电地平面后,造成信号最短回流路径断裂,此时,信号回路不得不绕道,形成很大的环路面积,电场和磁场的影响就变强了,容易干扰/被干扰。在分割区上跨接0欧电阻,可以提供较短的回流路径,减小干扰。
  *配置电路*
  一般,产品上不要出现跳线和拨码开关。有时用户会乱动设置,易引起误会,为了减少维护费用,应用0欧电阻代替跳线等焊在板子上。
  空置跳线在高频时相当于天线,用贴片电阻效果好。
  *其他用途* 布线时跨线
  调试/测试用
  临时取代其他贴片器件
  作为温度补偿器件
更多时候是出于EMC对策的需要。另外,0欧姆电阻比过孔的寄生电感小,而且过孔还会影响地平面(因为要挖孔)。

关于ARM栈初始化起始地址

堆栈寄存器起始地址的设置:

    程序刚启动并没有启动内存管理单元MMU,真正的内存地址如下S3C2440的储存空间映射图:

 

 

    ARM 使用统一编址,所以,我们得把堆栈指针设置到内存地址范围内;

    NAND FLASH 启动时:

        堆栈寄存器可以设为片内RAM 的最大地址:0x1000(4K)

        或者64MSDRAM的最大地址0x34000000(64M SDRAM的地址空间映射到BANK6,那么内存地址范围就是 0x30000000~0x34000000)。


    之所以选择最大地址是因为栈的生长方向是向下的,所以选最高地址作为栈的起始地址可以预防堆栈曾涨覆盖数据域;


关于linux下运行应用程序时候提示: -/bin/sh: xxx:not found的解决办法

关于mini2440上-/bin/sh: 命令:not found的解决办法

我按照mini2440的移植手册移植了linux内核和文件系统不同的是我用的交叉编译器是最新的4.4.1而没有用天嵌科技提供的交叉编译器,当我移植好了yaffs文件系统,想写个helloworld程序在开发板上测试下,我把编译好的helloworld文件放到yaffs文件系统的/usr/bin目录下,但当我运行/usr/bin/helloworld命令是提示“-/bin/sh: /usr/bin/helloworld: not found”,一开始我以为是helloworld没有运行权限,不过我给了它运行权限还是提示同样的错误。我在网上搜了下找到了原因:只所以提示“-/bin/sh: /usr/bin/helloworld: not found”这个,是因为我没有拷helloworld所需的库文件。那怎么才能知道helloworld需要哪些库文件呢,可以这样,在命令行输入arm-linux-readelf -a helloworld 命令然后在输出的内容中找到Program Headers:节这里就有helloworld所需的库文件如下图:

关于linux下运行应用程序时候提示: -/bin/sh: xxx:not found的解决办法 - 徐仁俊 - 徐仁俊的博客

看来我们需要ld-linux.so.3这个库,在你的交叉编译器中找到这个库文件把它拷到我们文件系统的/lib目录中然后烧到开发板中再次运行/usr/bin/helloworld结果提示“/usr/bin/helloworld: error while loading shared libraries: libgcc_s.so.1: cannot open shared object file: No such file or directory”有效果了,最起码不是前一个错误提示了,这就证明方法对头,我们看一下上面的错误这次直接提示所需的库文件了,我们按提示把libgcc_s.so.1拷到文件系统的/lib中,然后再次运行,又提示“/usr/bin/helloworld: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory”还是少库文件,我们再把这个也拷到文件系统的/lib中,这次总算是行了,终于看到“hello world”。

 


Linux内核链表简单应用

#include<linux/init.h>

#include<linux/module.h>

#include<linux/list.h>


/*Linux内核中有list_head原型如下

struct list_head 

{

struct list_head *next, *prev;

};

*/


//创建一个结构体保存学生信息

struct score

{

int num;

int math;

int eng;

struct list_head list;

};


struct score stu1,stu2,stu3;


struct list_head score_head;


struct list_head *pos;


struct score *tmp;


MODULE_LICENSE("GPL"); //申明遵守的许可证协议

MODULE_AUTHOR("XRJ"); //申明作者

MODULE_DESCRIPTION("First Module Program!"); //功能描述

MODULE_VERSION("V1.0"); //程序版本



static int mylist_init(void)

{

INIT_LIST_HEAD(&score_head); //Linux内核API创建链表

stu1.num=1;

stu1.math=90;

stu1.eng=80;

list_add_tail((&stu1.list),&score_head); //Linux内核API插入链表尾部

stu2.num=2;

stu2.math=100;

stu2.eng=80;

list_add_tail((&stu2.list),&score_head); //Linux内核API插入链表尾部

stu3.num=3;

stu3.math=60;

stu3.eng=99;

list_add_tail((&stu3.list),&score_head); //Linux内核API插入链表尾部

list_for_each(pos,&score_head); //Linux内核API遍历链表

{

tmp=list_entry(pos,struct score,list);

printk("num %d,eng is %d,math is %d\n",tmp->num,tmp->eng,tmp->math);

}

return 0;

}



static void mylist_exit(void)

{

list_del(&(stu1.list)); //Linux内核API链表节点删除

list_del(&(stu2.list)); //Linux内核API链表节点删除

list_del(&(stu3.list)); //Linux内核API链表节点删除

}


module_init(mylist_init);

module_exit(mylist_exit);

嵌入式汇编中的 REQUIRE8 和 PRESERVE8

REQUIRE和 PRESERVE这是字节对齐关键词,以前用ADS编译器的时候可以不用,但是keil编译器时需要加上REQUIRE8 指令指定当前文件要求堆栈八字节对齐。 它设置 REQ8 生成属性以通知链接器。

PRESERVE8 指令指定当前文件保持堆栈八字节对齐。 它设置 PRES8 编译属性以通知链接器。

链接器检查要求堆栈八字节对齐的任何代码是否仅由保持堆栈八字节对齐的代码直接或间接地调用。

语法

REQUIRE8 {bool} PRESERVE8 {bool}

其中:

bool

是一个可选布尔常数,取值为 {TRUE} 或 {FALSE}。

用法

如果您的代码保持堆栈八字节对齐,在需要时,可使用 PRESERVE8 设置文件的 PRES8 编译属性。 如果您的代码不保持堆栈八字节对齐,则可使用 PRESERVE8 {FALSE} 确保不设置 PRES8 编译属性。

Note

如果您省略 PRESERVE8 和 PRESERVE8 {FALSE},汇编器会检查修改 sp 的指令,以决定是否设置 PRES8 编译属性。 ARM 建议明确指定PRESERVE8。

您可以通过以下方式启用警告:

armasm --diag_warning 1546

有关详细信息,请参阅命令语法

您将会收到类似以下警告:

"test.s", line 37: Warning: A1546W: Stack pointer update potentially                                 breaks 8 byte stack alignment        37 00000044         STMFD    sp!,{r2,r3,lr}

示例

REQUIRE8 REQUIRE8 {TRUE} ; equivalent to REQUIRE8 REQUIRE8 {FALSE} ; equivalent to absence of REQUIRE8 PRESERVE8 {TRUE} ; equivalent to PRESERVE8 PRESERVE8 {FALSE} ; NOT exactly equivalent to absence of PRESERVE8

STM32汇编精确延时函数(调试)

/*SYS:72M*/

__asm void Delay_ms(R0)

{

PUSH   {R1} //2个周期 

DELAY_NMSLOOP

SUB    R0,#1 //R0=R0-1  

MOV    R1,#7199

DELAY_ONEUS

SUB    R1,#1

NOP

NOP

NOP

NOP

NOP

CMP    R1,#0

BNE    DELAY_ONEUS

CMP    R0,#0

BNE    DELAY_NMSLOOP

POP    {R1}

BX     LR //子程序返回

}


int main(void)

{

  RCC_Configuration();

  GPIO_Configuration();

  while(1)

  {

    GPIO_SetBits(GPIOD,GPIO_Pin_3);        

    Delay_ms(1000);

    GPIO_ResetBits(GPIOD,GPIO_Pin_3);        

    Delay_ms(1000);

  }   

}

关于Cortex—M3启动代码中堆栈对齐问题

一、什么是栈对齐?

栈的字节对齐,实际是指栈顶指针须是某字节的整数倍。因此下边对系统栈与MSP,任务栈与PSP,栈对齐与SP对齐 这三对概念不做区分。另外下文提到编译器的时候,实际上是对编译器汇编器连接器的统称。

之前对栈的8字节对齐理解的不透,就在网上查了好多有关栈字节对齐、还有一些ARM对齐伪指令的资料信息,又做了一些实验,把这些零碎的信息拼接在一起,总觉得理解透这个问题的话得长篇大论了。结果昨天看了AAPCS手册、然后查到了没有使用PRESERVE8伪指令出现错误的实例,突然觉得长篇大论不存在了,半篇小论这问题就能理顺了。

二、AAPCS栈使用规约

在ARM上编程,但凡涉及到调用,就需要遵循一套规约AAPCS:《Procedure Call Standard for the ARM Architecture》。这套规约里面对栈使用的约定如下:

5.2.1.1 
Universal stack constraints 
At all times the following basic constraints must hold: 
Stack-limit < SP <= stack-base. The stack pointer must lie within the extent of the stack. 
SP mod 4 = 0. The stack must at all times be aligned to a word boundary. 
A process may only access (for reading or writing) the closed interval of the entire stack delimited by [SP, stack-base – 1] (where SP is the value of register r13). 
Note 
This implies that instructions of the following form can fail to satisfy the stack discipline constraints, even when reg points within the extent of the stack. 
ldmxx reg, {..., sp, ...} // reg != sp 
If execution of the instruction is interrupted after sp has been loaded, the stack extent will not be restored, so restarting the instruction might violate the third constraint. 
5.2.1.2 
Stack constraints at a public interface 
The stack must also conform to the following constraint at a public interface: 
SP mod 8 = 0. The stack must be double-word aligned.

可以看到,规约规定,栈任何时候都得4字节对齐,在调用入口得8字节对齐。

在这个约定里,栈的4字节对齐确实得任何时候都遵守,而且你想不遵守都难,因为SP的最后两位是硬件上保持0的。而对于8字节对齐,这就需要码农和编译器配合着来。需要说明的一点是,8字节对齐即使不遵守,一些情况下也没问题,只要主调和被调用例程两边把堆栈使用,传参,返回等处理好就行,也就是说两边有自己的一套约定就行。但是有时候,主调这边在调用严格遵守AAPCS的函数时,没有将栈保持在8字节对齐上,那就会出问题。

三、如何编程?

在cortex m3上编程时,对于AAPCS栈使用约定的遵守,总的来说就两条:

1. 汇编文件中需要我们亲自动手来保证遵守AAPCS栈使用约定。

(特别注意每次从汇编进入C的世界时,要保证汇编部分的编码在调用c接口时栈是8字节对齐的,不要疏忽了,因为c编译器可不负责调整。c编译器说你得送给我的SP就是8字节对齐的,我才能保证接下来的C部分没有结束之前,遵守AAPCS栈使用约定)

2. 在C文件中,由编译器来处理。

四、补充:

1. 由于程序的入口点为复位中断响应函数,一般我们都写在启动代码里,通常是一个汇编文件,然后经由汇编进入到C程序的main入口处,在调用main的时刻,为遵循AAPCS,就得在此时保持8字节对齐。

2. 对于MSP,Keil MDK为我们提供了一个用来初始化C运行库环境的函数_main,这个函数会调用_user_setup_stackheap函数,该函数将MSP的低三位清零,然后在进入main之前不对其进行更改,这样在进入main的时刻,MSP保证为8字节对齐的。

3. 对于PSP,一般在上多任务OS时会用它,对于PSP我们要比MSP更为操心点,因为MSP起码还可以通过调用_main来跳进main的方式保证进入C世界的时候是遵守约定的。而PSP全靠自己来保证每次进入C世界时是8字节对齐。

4. 另外只要是汇编文件,可配合使用汇编命令armasm --diag_warning 1546,这样汇编器就会对一些SP没有8字节对齐的地方给出警告,但是我发现汇编器并不能保证检测到所有对SP造成8字节不对齐的操作,例如直接给SP载入一个立即数这种,汇编器就发现不了。我并没有对所有会影响SP的指令进行测试(原因是不熟悉。。。),不知道1546这个警告能覆盖多少指令,所以总的来讲,对汇编文件就是睁大自己的钛合金眼,争取大部分工作都放到C中去。

五. CORTEX-M3 中断控制器的栈对齐调整功能(该功能在r2p0版本以后的内核中均默认开启,STKALIGN位默认为1)

Cortex M3 NVIC CCR寄存器(控制与配置寄存器)的STKALIGN位置1,那么在发生中断时,进入中断响应函数前,内核会首先检查当前正在使用的栈指针是否8字节对齐,如果是,则正常将xPSR,PC,LR,SP,R0-R3入栈,如果不是,则先把SP-4,调整为8字节对齐,然后将xPSR第九位置1,接着把xPSR,PC,LR,SP,R0-R3入栈,再然后才进入中断响应函数。这样可以保证程序在运行过程中,如果在栈没有发生4字节对齐的地方发生中断了,进入到中断响应函数的时候也是遵守AAPCS栈使用约定的。如果中断服务程序是做任务切换的,那么前面的情况就是将任务栈调整为对齐,然后进入异常服务程序后使用系统栈,那如果系统栈本来就是不对齐的呢?通过中断来做任务切换的情况下,中断控制器并不会对系统栈进行调整,怎么办?其实这也不用担心,以μC/OS-II为例,在cortex-m3上通常使用PendSV异常来做任务切换,即将OSCtxSw以及OSIntCtxSw都设为仅完成PendSV异常触发功能,然后在PendSV异常服务程序中进行任务切换。由于上电时刻系统处于特权级模式,只要我们保证从上电开始到第一次系统调用,使用的栈都是系统栈MSP就可以了,这样即使第一次要进入任务切换时MSP不对齐,中断向量控制器也会给调整为8字节对齐状态,虽然这个第一次任务切换后除了中断再也不会使用MSP,但只要我们同时保证所有汇编部分都不会破坏8字节对齐规约,那么从此以后MSP都会是8字节对齐的。

六、关于ALIGN属性 与 PRESERVE8伪指令

在CORTEX M3芯片的启动代码中,这两个伪指令并非必不可少,可以不要这两个伪指令。但是有了这两个伪指令,可以在确保遵守AAPCS的道路上加一道保险,使得AAPCS栈使用约定的遵守在实际编程时变得稍微容易点。

当在段定义头(即AREA伪指令的相关代码)当中使用ALIGN=?时,ALIGN属性的作用为设定该代码段或数据段的首址的对齐位置,例如ALIGN=3就表示,该段首址将被安排在2^3=8字节对齐处。需要注意的是,除了AREA的ALIGN属性,还有一个同名的ALIGN指令,ALIGN指令使用在段内部的,用来调整ALIGN指令下一条命令或数据的对齐位置。

而PRESERVE8伪指令并不会对栈进行任何修改。PRESERVE8伪指令的使用有四种方法,分别如下,其中1、2的用法是等价的:

1.        PRESERVE8

2.        PRESERVE8 {TRUE}

3.        PRESERVE8 {FALSE}   

如果不写,那么由编译器来决定在编译过程中将汇编文件标识为PRES8属性还是~PRES8属性(也即加还是不加该伪指令),但经过实验,发现编译器在加不加这条伪指令上表现的并不完全可靠。。。所以最好明确的加上是 PRESERVE8 {TRUE}还是PRESERVE8 {FALSE}。那么这条伪指令起什么作用呢?

如果你想要告诉汇编器说:“在我这个汇编文件中保证栈的8字节对齐,我这个文件对栈的任何时刻的任何操作都是8字节对齐的”,那么你就把PRESERVE8伪指令用在汇编文件中,用以向汇编器通知前面你的保证内容。汇编器就知道你这个汇编文件是8字节对齐靠谱选手,将该文件标识为PRES8属性,然后如果在你这个汇编中调用了标示了需要8字节对齐属性的文件中的函数,连接的时候就不会报错。但是假如你把这个汇编文件标示为PRESERVE8 {FALSE},然后你又在这个文件中调用了标示了需要8字节对齐属性的文件中的函数,连接时就会给出错误信息。

那么什么是标示了需要8字节对齐属性的文件呢?如果你的某个汇编文件,某些操作一定要栈8字节对齐才行,那么你就需要使用REQUIRE8伪指令来通知汇编器将该文件标识为REQ8属性,然后这个文件就是所谓的“标示了需要8字节对齐属性的文件”。

在文件较多,文件之间调用由繁多的情况下,通过PRESERVE8和REQUIRE8的配合,就能够在连接期间由编译器检查出我们写代码时不小心造成的破坏8字节对齐模块对需要8字节对齐模块的调用(经过实验发现,汇编之间是给出警告,汇编调用C则是给出错误,由于C文件中并不能直接用REQUIRE8,所以我猜编译器将C文件都通通标识为REQ8属性了,所以才会出错)。

REQUIRE8的用法同PRESERVE8。

字对齐、半字对齐、字节对齐

一般情况下字为32位(4字节)、半字为16位(2字节)、字节为8位(1字节)。

大多数计算机使用 字节(8位的数据块)作为最小可寻址的存储器单位 ,而不是访问存储器中单独的位。存储器的每一个字节都由唯一的数字标识,称为该字节的地址,所有可能地址的集合称为存储器空间。

举例来说,ARM处理器工作状态有如下两种:

ARM状态:执行字对齐的32位ARM指令。

Thumb状态:执行半字对齐的16位Thumb指令。

字对齐、半字对齐、字节对齐只要明白其中一个,另外两个自然也就理解了。所以这里只对字对齐做一个解释:

假如,第一次取ARM指令1的地址为 0x0000 0000,由于ARM指令占32位(4个字节),因此地址0x0000 0001、0x0000 0002、0x0000 0003都是指令1的地盘。那么第二次取ARM指令2的地址为 0x0000 0004,同样的道理,0x0000 0005、0x0000 0006、0x0000 0007也都是指令2的地盘,以此类推:

指令1: 0x0000 0000 ——0x0000 0003

指令2: 0x0000 0004 ——0x0000 0007

指令3: 0x0000 0008 ——0x0000 000f

指令4: 0x0000 0010 ——0x0000 0013

观察各个指令的 起始地址 :

若按十进制来看分别是:0、4、8、16、… 都可以被4整除 。

若按二进制来看bit1和bit0都是0:也就是说它们的起始地址都是 0bxxxxxxxx xxxxxxxx xxxxxxxx xxxxxx 00 (32位地址)