乱码三千 – 分享实用IT技术

乱码三千 – 码出一个新世界


  • 首页

  • 归档

  • 搜索

视频帧率和码率对视频质量和文件大小的影响

发表于 2021-01-24

我们从画面流畅度,画面清晰度,和视频文件体积三个方面进行分析:

帧率(FPS)

指每秒显示图像的张数

比如30帧,表示每秒显示30张图像

帧率越大,画面越流畅,帧率越小,画面越卡顿,如果低到1帧/秒,那么就相当于一个幻灯片了

帧率不会影响到画面的清晰度,只会影响画面的流畅度和文件的体积,帧率越大,视频对应的体积越大

分辨率(resolution)

指像素点分布密度

比如24寸的显示器,设置1920x1080的分辨率,那就是横向由1920个像素点构成,纵向由1080个像素点构成,也就是说在屏幕尺寸固定的情况下,分辨率越高,画面越清晰细腻,反之,画面越模糊

分辨率不会影响画面的流畅度,只会影响画面的清晰度和文件的体积,分辨率越大,视频体积越大

码率(Bitrate)

指每秒传输的数据位数,单位kbps 即千位每秒 ,这里的位指的是二进制位

基本的算法是:

  • 每秒传输的数据量=码率/8

  • 视频文件体积=码率/8x视频时长(s) =每秒传输的数据量*视频时长(s)

比如500Kbps,也就是每秒传输62K大小的数据, 假如按照帧率30fps进行计算,那么每张图片大小约为2kb大小

也就是说视频时长固定的情况下,码率越高,文件体积越大,同时视频显示的有效像素越多,视频更加接近原始分辨率,反之,码率越低,文件体积越小,但是视频能显示的有效像素就越少,原本1080p的分辨率,每帧图像至少需要占用20k的数据量,但是码率过低每帧只能分配2K的量,此时像素点显示不全,就会造成视频模糊不清晰

因此,码率若是过低,再高的分辨率都拯救不了画质,反而会适得其反,

事实上,低码率环境下,低分辨率画面要比高分辨率画面更加清晰

码率不会影响画面的流畅度,但是会影响画面的清晰度和文件的体积

我们的需求

我们一般追求高清流畅的画质,同时文件体积尽可能小,那么该如何找到帧率 码率和分辨率三者的平衡点呢?

首先在我们录制视频的时候,屏幕的分辨率一般是固定的,随着不同的电脑分辨率也不同(当然你要手动调整也行)

其次,为了保证视频的流畅度我们一般将帧率设置在25fps以上

那么我们想减小视频的体积的话,只能从码率入手了, 找到一个画质能接受,体积小的平衡点

比如1980x1080的分辨率,我使用500kbps左右的码率,差不多可以接受,OBS在录制时默认也是在这个码率内来回波动

以下是常见分辨率和码率之间平衡参考表:

举例:如果要想百分百还原1080p HQ的画质,至少需要5.76Mkpbs码率的支撑,如果是直播的话需要至少9M的宽带才能稳定传输对应的数据量,否则画面卡顿

直播中,在分辨率和网速一定的情况下,我们会适当降低码率,牺牲部分清晰度的来保证视频的流畅性

如果网速不行又希望画面还清晰流畅,那么就降低拍摄的分辨率,同时降低数据传输量也就是码率值,当然分辨率降低之后所谓的视频清晰度也只是相对的,凡事总有取舍

本文为作者原创 转载时请注明出处 谢谢

img

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

关于mysql崩溃之error establishing a database connection

发表于 2021-01-22

起因

最近在进行网站检查的时候突然发现我的一个子网站挂了,提示error establishing a database connection, 如果是网站刚建立那好解决,80%的概率是数据连接参数不正确,或者是数据库端口没有开放

但是我这个网站运行很长一段时间了,之前一直好好的,突然之间就挂了, 而且这个数据库我使用的是docker进行维护,同时,在同一个mysql容器中我放置了好几个网站的数据,其他网站运行却是正常的

而且我通过第三方连接数据库的工具可以正常连接到被挂网站的数据库

通过以上情况可以排除以下原因:

  • mysql容器没有问题
  • 数据库端口和连接参数也没问题(已经检查过配置文件,没有被外部篡改过)
  • 网站程序正常运行

这种情况下该如何处理呢? 当然是找日志了

第一步:查看主程序日志,没问题

第二步:查看mysql容器日志,发现问题

1
docker logs --tail=50 mysql

Table './wordpress_wai/wp_options' is marked as crashed and should be repaired when using LOCK TABLES

数据库没有问题,但是表有问题

解决方案

对表进行修复即可:

首先登录mysql选择指定的数据库,然后执行以下命令:

  1. 执行修复指令

    1
    repair table wp_option
  2. 检查表的状态

    1
    check table wp_option

修复前最好先备份数据库

1
mysqldump -uroot -p密码 数据库名 >xxxxx.sql

如果表损坏比较严重,可能无法备份数据库

如果你不知道如何查看mysql日志,那么直接运行备份数据库指令,也能帮你检查数据库是否存在问题

修复完成后无需重启mysql容器, 直接刷新网址即可正常访问

2021年9月1日补充

突然数据库又崩了, 网站同样提示error establishing a database connection

我查看docker日志 报如下错误:

1
Database page corruption on disk or a failed file read of page [page id: space=0, page number=5]. You may have to recover from a backup

此时mysql容器已经进不去了, 百度了一下说是数据库受损 要在my.cnf配置文件中添加innodb_force_recovery=1

可是我在conf目录下并没有找到my.cnf文件 不知道是被谁给删了 难道被入侵了

只好新建一个配置文件看看可不可行

1
vim my.cnf

然后将以下内容拷贝进去:

1
2
3
4
5
6
7
8
9
10
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
secure-file-priv= NULL
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0

# Custom config should go here
!includedir /etc/mysql/conf.d/

wq保存退出 然后重启mysql容器, 重新访问网站 还是不行

紧接着我在配置文件末尾添加了innodb_force_recovery=1

1
2
3
4
5
6
7
8
9
10
11
12
13
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
secure-file-priv= NULL
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0

# Custom config should go here
!includedir /etc/mysql/conf.d/


innodb_force_recovery=1

保存重启后, 网站正常访问, 问题解决

但是mysql容器日志一直提示:

1
[ERROR] [MY-012803] [InnoDB] innodb_force_recovery is on. We do not allow database modifications by the user. Shut down mysqld and edit my.cnf to set innodb_force_recovery=0

于是我把innodb_force_recovery值设为0 网站没问题 暂且就这样

关于innodb_force_recovery参数

  • Mode 1当遇到损坏页时,不使 MySQL 崩溃
  • Mode 2不运行后台操作
  • Mode 3不会尝试回滚事务
  • Mode 4不计算统计数据或应用存储/缓冲的变化
  • Mode 5在启动过程中不查看撤消日志
  • Mode 6在启动时不从重做日志(ib_logfiles)前滚

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

GCC调试工具GDB的常用指令

发表于 2021-01-20

常用指令

  1. 进入调试模式

    1
    gdb 可执行文件
  2. 如果忘了指定调试文件可以使用file指令指定

    1
    (gdb) file 文件名
  3. 使用quit指令退出调试模式 可简写为q

    1
    (gdb) q
  4. 使用start指令开始调试 停在第一行代码处

    1
    (gdb) start
  5. 使用step指令进行单步执行 可简写为s

    1
    (gdb) s   #会进入函数内部
  6. 使用next执行进行单步执行 可简写为n

    1
    (gdb) n  #不会进入函数内部
  7. 汇编级别单步执行(上面n和s为c语言级别单步指令)

    1
    2
    (gdb) ni #不进入函数内部
    (gdb) si #会进入函数内部
  8. 使用list指令列出所有源代码 可简写为l

    1
    (gdb) l
  9. 使用break指令设置断点位置 可简写为b

    1
    2
    3
    4
    5
    (gdb) b main #在main函数处打断点
    (gdb) b 10 #在第十行位置打断点
    (gdb) b test:10 #在test文件的第十行位置打断点
    (gdb) b 0x3400a #在0x3400a内存位置打断点
    (gdb) b 10 if i==3 #设置条件断点 i等于3时在第十行位置打断点 适用于循环
  10. 使用delete+断点编号指令删除断点 可简写为d

    1
    (gdb) delete 3 #删除编号为3的断点  清除时GDB不会给出任何提示
  11. 使用 clear+断点行号指令清除断点

    1
    (gdb) clear 3 #清除第三行的断点  清除时GDB会给出提示
  12. 使用 disable/enable + 断点编号 指令冻结或启动断点

    1
    disable 3,4 #冻结编号为3和4的断点 多个断点使用逗号分隔
  13. 使用info break指令查看断点的情况

    1
    (gdb) info break
  14. 使用 tbreak 指令设置临时断点

    1
    (gdb) tbreak 行号/函数名 #设置临时断点,到达后被自动删除
  15. 使用 awatch/watch + 变量 设置变量读写观察点

    1
    2
    (gdb) awatch/watch a #当变量a被读出或写入时程序被暂停 
    (gdb) rwatch a #当变量a被读出时程序被暂停
  16. 使用continue指令运行到断点处 可简写为c

    1
    (gdb) c
  17. 使用print指令打印变量的值 可简写为p

    1
    (gdb) p a #打印变量a的值
  18. 使用x指令打印指定内存地址数据

    1
    (gdb) x /6cb 0x804835c #打印地址0x804835c起始的内存内容,连续6个字节,以字符格式输出。
  19. 使用run指令运行整个程序 可简写为r

    1
    (gdb) r  #如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处
  20. 使用 call 指令直接运行某个函数

    1
    (gdb) call fun #在当前位置执行函数fun
  21. 使用display 指令设置需要跟踪的变量

    1
    (gdb) display a #跟踪变量a 每次断点到该处就显示该变量的值
  22. 使用 info display 显示当前所有跟踪的情况

    1
    (gdb) info display
  23. 使用undisplay+编号 指令取消对变量的跟踪

    1
    (gdb) undisplay 3 #取消对编号为3的跟踪事件
  24. 使用 set+ 变量 指令改变变量的值

    1
    (gdb) set i=3 #临时设置变量i的值为3
  25. 使用 set 指令设置运行时参数

    1
    (gdb) set i=3 #临时这只变量i的值为3
  26. 使用 show 指令查看运行时参数

    1
    (gdb) show i
  27. 使用 finish 指令函数结束

    1
    (gdb) finish
  28. 使用help指令查看指令使用说明

    1
    2
    (gdb) help print #查看print指令的解释说明
    (gdb) help #查看所有指令
  29. 使用info reg指令查看寄存器状态

    1
    (gdb)  info reg
  30. 使用 info stack指令查看堆栈状态

    1
    (gdb) info stack
  31. 运行shell指令

    1
    (gdb) shell ls   #运行shell指令ls。
  32. 其他指令

    1
    2
    3
    4
    5
    6
    7
    (gdb)  path #可设定程序的运行路径。 
    (gdb) show paths #查看程序的运行路径。


    (gdb)  cd   #相当于shell的cd命令。

    (gdb)  pwd  #显示当前的所在目录
  1. 使用回车重复上一条指令

本文为作者原创 转载时请注明出处 谢谢

img

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

视频教程录制计划

发表于 2021-01-17

技术视频

  1. 《Android进阶之逆向安全反编译视频教程-胖薯出品》

    • 录制时间 2020年11月6日
    • 部分视频观看:点击跳转
  2. 《Smali语言从入门到精通视频教程-胖薯出品》

    • 录制时间 2020年11月6日
    • 部分视频观看:点击跳转
  3. 《程序员进阶之三大架构汇编语言入门视频教程-胖薯出品》

    • 录制时间 2020年12月14日
    • 部分视频观看:点击跳转
  4. 《ARM汇编进阶之ARM64汇编视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  5. 《MIPS汇编进阶之MIPS64汇编视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  6. 《X86汇编进阶之Win32汇编视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  7. 《X86汇编进阶之Win64汇编视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  8. 《lua语言从入门到实战视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  9. 《硬件开发入门视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  10. 《从0到1打造一门属于自己的编程语言视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  11. 《从0到1开发一款操作系统视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  12. 《音乐制作之视唱练耳视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  13. 《虚幻游戏开发从0到1视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  14. 《音频插件开发视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  15. 《SO库的反编译视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:
  16. 《360和腾讯加固的原理和脱壳视频教程-胖薯出品》

    • 录制时间
    • 部分视频观看:

大家好 我已入驻面包多, 后续的付费教程会在上面发布 感谢大家的支持, 主页地址 : https://mbd.pub/o/newban/work

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

从0到1打造一门属于自己的编程语言(一)

发表于 2021-01-17

前言

学习编程语言的开发,目的不是为了造轮子,而是为了了解程序语言的本质和原理,方便我们日常的开发

学习此课程前需要提前掌握以下知识:

  • 了解汇编语言
  • 熟悉至少一门高级编程语言

编程语言的来源

我们如果想要和外国人交流,那么我们必须学会外语, 或者让外国人学咱们的语言

我们如果想要指挥计算机,那么我们必须学会计算机语言,让计算机学咱们的语言不太可能,至少现在不行!

很多人都认为我们平常使用的编程语言比如C语言 Java语言等就是计算机语言,这种说法不太严谨, 因为计算机压根不认识编程语言,它只认识二进制码, 也就是说 二进制码才是计算机真正的语言

那我们要指挥计算机岂不是得学二进制码(机器码),在编程语言发明之前,确实是如此

科技的进步来源于懒惰

人类势必不会甘愿长期忍受机器码的摧残, 为了摆脱效率低下的编码,于是乎,聪明的人类发明了编程语言,比如汇编, 相比机器码汇编显然要舒服的多, 但是随着时间的推移,人们发现汇编语言存在非常严重的弊端, 最突出的一点就是不同cpu架构需要制定一套不同指令集,通俗一点就是不同cpu对应着不同的一套汇编语言,这就导致了无法跨平台

有人会问,为什么当时大家不使用同一种cpu呢, 历史不能预知未来的发展,每个时代都有各自的商业竞争

新技术的出现往往是因为问题的长期累积

于是乎,跨平台语言C语言问世了, 但是C语言知识语言跨平台,但是其编写的程序并不跨平台

于是乎,可以程序跨平台的Java语言问世了

……..

从这段历史,我们不难发现:

编程语言只是一个方便人类与计算机交流的工具

只是工具,仅此而已

我们现在花大量时间和精力所学所用的都是别人制作出来的工具

当我们因熟练掌握几门编程语言而洋洋得意时,是否有想过,我们只是一个工具的熟练运用者

我并不是说学语言很low, 毕竟编程语言的门槛也不低,需要花大量时间和精力才能有效掌握,我只是想说,一旦你明白了语言的创造过程, 那就等于你掌握了现在市面上所有的编程语言, 所有的语言,原理都是一样的,只不过语法和关键字不同,仅此而已

也就是说,我们需要去了解工具的生产过程

思考

如果你去网上搜相关编程语言制作的视频或资料, 基本上都是词法分析,语法分析,语义分析等等, 让人一头雾水, 直接劝退

试想一下,在若干年前编程语言还没有问世的时候,有词法语法语义符号token等等这一些个含义么? 完全没有!

这些理论都是前人经验总结而成, 经验固然很好,能少走弯路,但是对于初学者来讲极其不友好

只有从初学者的角度出发,才能更平滑地学习到原本复杂的知识

因此抛开这些专业术语吧,从0开始出发

起步

假如现在编程语言还未问世,你现在要自创一门语言方便人类开发, 你会怎么做?

比如我想让计算机帮忙算个数,计算1+1

原本使用机器码可能得这样写:

1
4F 9B 55 7C 2D 3A  ;实际过程中我们一般使用十六进制进行表示,cpu执行的时候执行的是对应的二进制

此时,我可能会考虑用一句话来代替这个计算1加1的功能,比如

1
加:1+1   ;这是不是好理解多了

问题来了, 如果将我这句话转成机器码呢? 也就是说,我们需要将这句话翻译成机器码, 那怎么翻译呢? 是不是得需要一个翻译工具呀

于是乎,我们与此同时需要想办法整一个翻译工具, 由于我设计的这句话是敲在计算机上的,而不是写在纸上的, 如果是写在纸上的,兴许我们可以造一台机器将我写的内容转印成机器码,然后机器码敲入计算机:cry:… 这种愚蠢的做法, 简直就是科技的倒退

既然是直接敲在计算机中,那我们的翻译工具必然也是一个能被cpu执行的程序, 那我们需要先编写翻译工具这个程序的机器码

理论上是这么个逻辑

我们需要一边指定语法规则,一边编写和优化的翻译程序,因为我们的需求不单单是让计算机做加法运算

这个能把我们指定的语言翻译成机器码的翻译工具,就是当今所说的编译器,后文皆以编译器替代

编译器该如何写

由于源代码都是存放在文件中,按照我们的正常思维,最先想到的是将这个文件中的内容读取出来,然后从头到尾进行匹配判断,根据不同的关键字判断不同的功能,然后用与之对应的机器码进行替换

cpu在进行文件读取的时候,本质上读取的是二进制,那我们编写的中文或者英文和二进制数据的一一对应关系就涉及到编码格式问题,常见的编码格式有:

  • ASCII码 (只识别英文)
  • GBK (识别英文和中文)
  • UTF-8 (识别各个国家的语言)
  • …

假如我们接下来要使用UTF-8进行编码,那么我们需要指定一张表,也就是二进制码和功能之间的一一对应关系,比如算术运算中加这个关键字:编译器从文本中连续读取两个字节二进制数据然后查表,如果对应上了加法功能,那么继续往后读取需要进行运算的内容,读到结束标记的时候,然后将之前所读取的二进制转化成与之对应功能的机器码,以此类推,这个结束标记也是由我们来定,我们可以使用分号、回车换行或者其它特殊的符号作为一行语句的结束

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

硬件开发之个人PCB电路板制作

发表于 2021-01-07

流程

第一步 画电路原理图

  1. 工具

    Altium Designer 19: 中文官网:https://www.altium.com.cn/products/downloads

第二步 联系PCB厂家

  1. 廉价厂家推荐
    • 嘉立创 :10x10cm的板子5张 只需5块钱
    • 捷配 :10x10cm的板子5张 19块钱

第三步 电子元器件采购

第四步 电子元器件焊接

第四步 电路板测试

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

硬件开发之基础知识(-)

发表于 2021-01-06

前言

随着智能家居的兴起,5G时代的到来, 智能硬件扮演者越来越关键的角色, 对于老百姓的我们而言,占据先机最好的办法的就是了解并掌握它, 因为知识是公平的

基础物理知识

这些我们在学生时代都学过,现在需要把它们都重新拾起来

1. 电压

可以理解为电子移动的动力,压力即是动力,单位:V

2. 电流

电荷的定向流动 ,犹如水流

流动的电才能将电灯点亮

单位:A

3.电的产生

产生电的方式有很多种,摩擦生电,电解质生电,一般我们日常使用的电,是通过发电机发电,它的原理是:

闭合电路的一部分导体在磁场里做切割磁感线的运动时,导体中就会产生电流

材料:

1. 磁铁
 2. 线圈

自制发电机:https://v.163.com/static/1/VYUIDVSRG.html

电子元器件

分类:

  • 元件:工厂在加工时没改变原材料分子成分的产品可称为元件。元件属于不需要能源的器件。它包括:电阻、电容、电感等
  • 器件:工厂在生产加工时改变了原材料分子结构的产品称为器件。器件需要消耗能源,它包括双极性晶体三极管 场效应晶体管 可控硅 等

常见电子元器件

  1. 电阻
  2. 电容
  3. 二极管
  4. 三极管
  5. 继电器
  6. 电容器
  7. 电位器
  8. 传感器

电流的的流向判断

第一步:判断是否有一条电路直接将正负极向连,如果有则短路,没有则进入第二步

第二步:在电流分叉路口,判断是否有一条电路没有连接任何元器件,如果有则走这条路,其他路不通, 如果都连接了器件,那么都有电流通过

串联和并联电路

1.串联电路的特点:
(1)电流:文字表达:串联电路中各处电流都相等.
公式表达:I=I1=I2=I3=……=In
(2)电压:文字表达:串联电路中的总电压等于各部分电路电压之和.
公式表达:U=U1+U2+U3+……+Un
(3)电阻:文字表达:串联电路中的总电阻等于各部分电路的电阻之和.
公式表达:R=R1+R2+R3+……+Rn
(4)分压定律:文字表达:串联电路中各部分电路两端电压与其电阻成正比.
公式表达:U1/R1=U2/R2=U3/R3=.=Un/Rn
另种表达:U1:U2:U3:…:Un= R1:R2:R3:…:Rn
特例:U1/U2=R1/R2
2.并联电路的特点:
(1)电流:文字表达:并联电路中总电流等于各支路中的电流之和.
公式表达:I=I1+I2+I3+……+In
(2)电压:文字表达:并联电路中各支路两端的电压都相等.
公式表达:U=U1=U2=U3=……=Un
(3)电阻:文字表达:并联电路总电阻的倒数等于各支路电阻倒数之和.
公式表达:1/R=1/R1+1/R2+1/R3+……+1/Rn
(4)分流定律:文字表达:并联电路中,流过各支路的电流与其电阻成反比.
公式表达:I1R1=I2R2=I3R3= …=InRn
特例:I1/I2=R2/R1

总结

在串联电路中

  • 电阻越大,截取的电压越大

在并联电路中

  • 电阻越大, 截取的电流越小
  • 分支电路越多,总电流越大

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

关于字节和字长以及半字的关系

发表于 2021-01-05

关于字节

一个字节是固定为8个二进制位

关于字(word)

字的长度是可变的,它和cpu一次性能操作的实际位数有关

比如16位的cpu一次能操作16位数,因次2个字节代表一个字

如果是32位cpu,那么32位也就是4个字节代表一个字的长度(word)

关于半字

半字即为字长的一半,在16位cpu中,半字即为一个字节,故在16位中没有半字这个说法,一半32位以上cpu环境中常用

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

ARM原生汇编与ARM GNU汇编的区分

发表于 2021-01-05

一、ARM汇编开发的两种的方式

ARM汇编开发指用ARM提供的汇编指令,进行ARM程序的开发。

ARM汇编开发,有两种开发方式,一种是使用ARM汇编,一种是使用ARM GNU汇编。两种汇编开发,使用的汇编指令是完全一样的,区别是宏指令,伪指令,伪操作不一样。其实两种开发方式的区别在于所使用的编译工具不一样。

对于ARM汇编,使用的是ARM公司开发的编译器,而ARM GNU汇编,是使用GNU为ARM指令集开发的编译器,也就是arm-gcc。

二、ARM的编译开发环境

两种常用的ARM的编译开发环境

  • DS5:ARM提供的集成开发软件。使用的是ARM提供的工具链进行程序编译
  • GNU开发环境: 由GNU的汇编器as,交叉编译器gcc,和链接器ld等组成

三、伪操作,宏指令,伪指令

伪操作:ARM汇编语言程序里的一些特殊指令助记符,其作用主要是完成汇编程序做各种准备工作,在源程序进行汇编时由汇编程序处理,而不是在计算机运行期间由机器执行。如程序段的定义,就属于伪操作。

宏指令:一段独立的程序代码,可插在源程序中,通过伪操作来定义。

伪指令:ARM汇编语言程序里的一些特殊指令助记符,不在处理器运行期间执行,在汇编时,被合适的ARM的机器指令代替,从而实现真正的指令操作。

四、ARM原生汇编伪操作

伪操作 语法格式 作用
GBLA GBLA Varible 声明一个全局的算术变量,并将其初始化为0
GBLL GBLL Varible 声明一个全局的逻辑变量,并将其初始化成{FALSE}
GBLS GBLS Varible 声明一个全局的字符串变量,并将其初始化成空串
LCLA LCLA Varible 声明一个局部的算术变量,并将其初始化为0
LCLL LCLL Varible 声明一个局部的逻辑变量,并将其初始化成{FALSE}
LCLS LCLS Varible 声明一个局部的字符串变量,并将其初始化成空串
SETA SETA Varible expr 给一个全局或局部算术变量赋值
SETL SETL Varible expr 给一个全局或局部逻辑变量赋值
SETS SETS Varible expr 给一个全局或局部字符串变量赋值
RLIST name LIST {list of registers} 为一个通用寄存器列表定义名称
CN name CN expr 为一个协处理器的寄存器定义名称
CP name CP expr 为一个协处理器定义名称
DN/SN name DN/SN expr DN/SN为一个双精度/单精度的VFP寄存器定义名称
FN name FN expr 为一个FPA浮点寄存器定义名称
LTORG LTONG 声明一个数据缓冲池(文字池)的开始
MAP MAP expr {, base-register} 定义一个结构化的内存表(storage map)的首地址
FIELD {label} FIELF expr 定义一个结构化内存表中的数据域
SPACE {label} SPACE expr 分配一块连续内存单元,并用0初始化
DCB {label} DCB expr {,expr}.. 分配一块字节内存单元,并用expr初始化
DCD/ DCDU {label} DCD/DCDU expr {,expr}… 分配一块字内存单元, 并用expr初始化
DCDO {label} DCDO expr {,expr}… 分配一块字对齐的字内存单元, 并用expr初始化
DCFD/DCFDU {label} DCFD{U} fpliteral ,{,fpliteral}… 为双精度的浮点数分配字对齐的内存单元
DCFS/DCFSU {label} DCFS{U} fpliteral,{,fpliteral}… 为单精度的浮点数分配字对齐的内存单元
DCI {label} DCI expr, {expr}… ARM代码分配一段字对齐的内存单元,填充expr(二进制指令码),THUMB代码中,分配一段半字对齐的半字内存单元。
DCQ/ DCQU {label} DCQ{U} {-} literal, {, {-} literal}… 分配一段以双字(8个字节)为单位的内存
DCW/DCWU {label} DCW{U} {-} literal, {, {-} literal}… DCW用于分配一段半字对齐的半字内存单元

1、AREA

创建一段新的程序代码或数据区。

格式 :

1
AREA  name, {,attr,} …

其中,name是程序段名, atrr是段名属性

对于属性,有以下一些:

  • CODE: 用于定义代码段,默认为是READONLY
  • DATA: 用于定于数据段,默认为READWRITE
  • READONLY: 指定本段的内容只读
  • READWRITE: 指定本段的内容可读可写
  • ALIGN: 指定对齐为2次幂
  • COMMON: 定义通用的段。不包含任何用户的代码和数据。各源文件中同名的COMMON属性段共享同一段存储单元

2、ALIGN

指定对齐

ALIGN 4 表示4字节地址对齐

ALIGN 8 表示8字节地址对齐

注意:在AREA中使用和单独使用ALIGN的区别,在于格式和对齐的计算不一样。

3、ENTRY

指定汇编程序的入口。

一个程序至少有一个入口点,也可以有多个入口点,但是在一个源文件中,最多只能有一个ENTRY。当多个源文件均有ENTRY时,由链接器指定程序真正的入口。

4、END

表示源程序的结束

所以汇编语言源文件必须以END结束,汇编器遇到END, 将结束编译。

5、EXPORT

格式: EXPORT 标号 [,WEAK]

声明一个全局标号,其他源文件可以使用这个标号。WEAK表示碰上其他同名标号时,其他标号优先。

6、IMPORT

格式: IMPORT 标号,[,WEAK]

表示该引用的标号在其他源文件中,单要在当前文件中引用。WEAK表示找不到该标号时,也不报错,一般该标号置为0,如果是B 或BL指令用到该标号,该指令置为nop。

该标号会加入到当前源文件的符号表中。

7、EXTERN

和IMPORT一样,不同在于,如果当前文件没有引用该标号,该标号不会加入到当前源文件的符号表中。

8、GET(或INCLUDE)

将一个源文件含到当前的源文件中

9、EQU

对一个常量标号赋值

格式:

1
name  EQU  expression

其中: name符号名, expression寄存器相关或者程序相关的固定值

如:

1
num  EQU  2  ;  为符号赋予数字2

EQU,等同于C语言中用#define定义一个常量

10、SPCAE

用于分配一片连续内存单元,并用0初始化。SPACE可用%代替。

格式:

1
{label} SPACE expr

label : 是一个标号, 可选

expr: 分配的内存字节数

如

1
stack SPACE 100 ; 分配100个字节内存单元,并用0初始化。标号stack是这片空间的起始地址

11、DCB

用于分配段字节内存单元,并用伪操作中的expr初始化。

格式:

1
{label} DCB expr {,expr}

label: 是一个标号,可选

expr: 可以是-128~255的数值或者字符串

如:

1
string  DCB  "HELLO"  ;为HELLO字符串分配空间, string是这块空间的起始地址

12、DCD及DCDU

用于分配段字内存单元(分配的内存都是字对齐,DCDU并不严格字对齐),并用伪操作中的expr初始化。 DCD 可用 & 代替。

格式:

1
{label} DCD expr, {,expr}

label: 是一个标号,可选,表示这块内存单元的首地址

expr: 数字表达式或程序中的标号

如:

1
data DCD  1,2,3,4   ;分配字对齐的字单元空间,初始化为1,2,3,4

五、ARM原生汇编伪指令

ARM伪指令包括: ADR, ADRL,LDR ,NOP

THUMB伪指令包括:ADR, LDR, NOP

伪指令 语法格式 作用
ADR ADR{cond} register, expr 将基于PC或基于寄存器的地址值读取到寄存器中。小范围的地址读取
ADRL ADRL{cond} register, expr 将给予PC或基于寄存器的地址值读取到寄存器中。中等范围的地址读取
LDR LDR {cond} register, =[expr|label] 将一个32位的立即数或者一个地址值读取到寄存器中。大范围的地址读取
NON NOP 在汇编时,被替换成空操作

六、ARM GNU编译环境

伪操作 语法格式 作用
.byte .byte expr {,expr}… 分配一段字节内存单元,并用expr初始化
.hword/.short .hword expr {,expr}… 分配一段半字内存单元,并用expr初始化
.ascii .ascii expr {,expr}… 定义字符串expr
.asciz/.string .asciz expr {,expr}… 定义字符串expr(会增加/0为结束符)
.floar/.single .float expr {,expr}… 定义32bit IEEE浮点数expr
.double .doubel expr {,expr}… 定义64bit IEEE浮点数expr
.word/.long/.int .word expr {,expr}… 分配一段字内存单元,并用expr初始化
.fill .fill repeat {,size} {,value} 分配一段字节内存单元,用sieze长度value填充repeat次
.zero .zero size 分配一段字节内存单元,并用0填充内存
.space/.skip .space size, {,value} 分配一段内存单元,用value将内存初始化
.section .section expr 定义一个段
.text .text {subsection} 代码段,
.data .data{subsection} 数据段
.bss .bss{subsection} bss段
.cond 16/.thumb .code 16/.thumb 表示之后的汇编指令使用THUMB指令集
.code 32/.arm .code 32/.arm 表示之后的汇编指令使用ARM指令集
.end .end 标记汇编文件的结束
.include .include “filename” 将一个源文件包含到当前源文件中
.align/.balign .align {alignment} {,fill},{max} 通过填充字节使当前位置满足一定的对齐格式

七、两种开发环境的区别

两种开发环境下的汇编代码,有较多不同的点,主要是符号及伪操作的不同。

ARM汇编的伪操作符 GNU汇编的伪操作符
INLCUDE .include
NUM EQU 25 .equ NUM, 25
EXPORT .global
IMPORT .extern
DCD .long
IF: DEF: .ifdef
ELSE .else
ENDIF .endif
OR |
SHL <<
RN .req
GBLA .global
NUM SETA 16 .equ NUM , 16
MACRO .macro
MEND .endm
END .end
AREA WORD, CODE, READONLY .text
AREA BLOCK, DATE, READWRITE .data
CODE32 .arm
CODE16 .thumb
LTORG .ltorg
% .fill
ENTRY ENTRY:
ldr x0,=0xff ldr x0,=0xff

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

汇编语言之ARM32汇编

发表于 2021-01-05

ARM两种编译环境

两种常用的ARM的编译开发环境

  • ARM原生编译环境:ARM官方提供的原生编译环境,相关集成开发软件有ADS,Keil等,常用于ARM单片机开发
  • GNU编译环境: 由GNU的汇编器as,交叉编译器gcc,和链接器ld等组成,一般适用于交叉编译环境需求

以上两种编译环境,使用的指令集都是一致的, 只是语法格式有不同,也就是宏指令,伪指令,伪操作不一样

ARM原生环境搭建

使用 Keil μVision5 这款软件进行ARM32的汇编学习

下载地址:http://www.mcuzone.com/down/Software.asp?ID=10000503

ARM32系列命名

ARM产品 ARM架构
ARM7 ARM v4
ARM9 ARM v5
ARM11 ARM v6
Cortex-A ARM v7-A
Cortex-R ARM v7-R
Cortex-M ARM v7-M

寄存器

在ARM32中一共有37个寄存器,其中包含16个通用寄存器(R0~R15)和1个状态寄存器 ,15个通用影子寄存器,5个状态影子寄存器

影子寄存器

如上图所示,在ARM32中一共有7中不同的处理器模式,分别为:用户模式(User),快速中断模式(FIQ),普通中断模式(IRQ),管理模式(Svc),数据访问中止模式(Abort),未定义指令中止模式(Und),系统模式(Sys)

但是在不同的模式下,同样的一个寄存器名称指向不同的物理寄存器,这些不同的物理寄存器就被称之为影子寄存器

由于这些影子寄存器也属于通用寄存器的范畴, 因此很多人也将ARM32的通用寄存器归纳为31个

语法

  1. 注释(两种方式)

    1
    2
    3
    ; 我是注释

    /*…我是注释..*/
  2. 声明一个代码段

    1
    2
    3
    4
    5
    6
    7
    8
     	AREA test, CODE	 ;声明一个代码段,段的名称为test(名称可自定义),CODE关键字大小写都可,为了区分,一般大写

    ;=========在此编写汇编代码==========

    END ;END表示编译结束标记


    ;段的声明需要以制表符开头,前面留出空
  3. 数据表示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mov R0,#13 ;将十进制13赋值给R0寄存器

    mov R0,#0x13 ;将十六进制0x13赋值给R0寄存器


    mov R0,#8_12 ;将8进制数12赋值给R0寄存器 N进制则为#N_xxx


    mov R0,#'a' ;将字符a对应的ascii码值传送给R0寄存器
  4. 函数声明和调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
      	AREA test, CODE


    bl print ;函数调用

    print ;函数名称
    mov R3,#5
    bx lr
    END ;END表示编译结束标记
  1. 声明一个数据段

    1
    2
    AREA da, DATA	 ;声明一个数据段 默认可读可写状态
    ;数据定义伪操作
  1. 数据定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # DCB   
    Str DCB "This is a test!" ;分配一片连续的字节存储单元并初始化。取名为Str 类似于8086中的db
    Str = "This is a test!" ;DCB也可用等号代替


    #DCW
    Str DCW 1,2,3 ;定义字型数据 每个字符占用2个字节的空间


    #DCD
    Str DCD 1,2,3 ;定义半字型数据 每个字符占用4个字节的空间

    字符串必须使用DCB进行定义

  2. 分配一块连续的内存空间

    1
    2
    3
    4
    sp1 SPACE 100 ;分配一块连续100个字节的空间

    ;或者使用%代替SPACE简写
    sp1 % 100 ;分配一块连续100个字节的空间

代码编写规范

  1. 所有指令和伪指令不允许顶格
  2. 所有变量和标签必须顶格
  3. 一般我们将伪指令大写,变量和标签小写

内存数据的读写

  1. 从内存中读取数据

    使用中括号表示通过地址取值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    LDR R0,[R1]               ;将内存地址为R1的字数据读入寄存器R0。
    LDR R0,[R1,R2] ;将内存地址为R1+R2的字数据读入寄存器R0。

    LDR R0,[R1,#8] ;将内存地址为R1+8的字数据读入寄存器R0。
    LDR R0,[R1,R2]! ;将内存地址为R1+R2的字数据读入寄存器R0,并将新地 址R1+R2写入R1。



    LDR R0,[R1,#8]! ;将内存地址为R1+8的字数据读入寄存器R0,并将新地址 R1+8写入R1。

    LDR R0,[R1],R2 ;将内存地址为R1的字数据读入寄存器R0,并将新地址 R1+R2写入R1。


    LDR R0,[R1,R2,LSL#2]! ;将内存地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。


    LDR R0,[R1],R2,LSL#2 ;将内存地址为R1的字数据读入 寄存器R0,并将新地址R1+R2×4写入R1。


    ;从标号即为地址
    LDR R0,label ;将标号对应的内容赋值给R0

复杂格式如LDR R0,[R1],R2,LSL#2 其中 []运算优先
2. 向内存中写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STR R0,[R1],#8           ;将R0中的字数据写入以R1为地址的内存中,并 将新地址R1+8写入R1。


STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的内存中。

STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的内存中。
STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。
STR R0,[R1],#8 ;将R0中的字数据写入以R1为地址的内存中,并 将新地址R1+8写入R1。


STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的内存中。

STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的内存中。
STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。

LDR伪指令

这个指令和内存读取指令长的一模一样,如果我们在使用的时候加个等号,那么它就是另外一个指令

1
2
3
4
5
6
;如果不加等号 是内存读取功能
LDR R0,label ;获取标签所对应的内存数据赋给R0

;一旦加了等号,则变成了传送指令
LDR R0,=label ;将标号对应的实际物理地址值赋值给R0 此时它的作用和mov无异
LDR R0,='a' ;直接将字符数据传送给R0

实际上,加了等号的LDR指令,刚好可以弥补mov指令的不足, mov指令只能传送由八个二进制位右移而得的数据,而LDR则没有这个限制

也就是说如果我们想将一个数值传入寄存器,可以有两种方式:

1
2
3
4
5
6
7
8
9
10
;第一种
mov R1,#0X100

;第二种
ldr R1,=0x100

;mov指令的限制:只能传送由八个二进制位右移而得的数据, 也就相当于是两个十六进制数据,由于可以不断移位那么数据的大小可以伸缩,比如以下数据都可使用mov指令
0x00000058 0x00000580 0x00005800 0x00058000 0x00580000 0x05800000 0x58000000
;我们发现一个规律:mov指令只能传送最大两个十六进制空间的数据,注意是空间,这两个数据随意你移动,一旦不满足这个条件则无法传送,比如
0x00000581

LDR伪指令总结

作用:

  • 弥补mov指令的不足
  • 获取数据所对应的内存地址

ADR指令

那么除了通过LDR伪指令来获取数据所在地址外还有一个指令也可以获取数据地址,那就是adr指令,但这个指令只能获取当前段内数据的地址,段外数据无法获取,ldr则没有这个限制

1
2
3
4
5
	AREA test2,CODE
mov R3,#8
aaaa dcb 1,2,3
adr R0,aaaa ;获取aaaa首地址
END

段的拓展

段属性拓展

  1. 段读写属性
  • READONLY

    该段内存区域数据只能读取,不能写入,也就是如果使用内存读写指令,数据也写入不了

  • READWRITE

    该段内存区域可读可写,不仅可以使用内存读写指令,还可以在调试的时候直接在memory窗口双击修改

示例:

1
2
AREA code, CODE,READWRITE ;将代码段内存区域设置为可读可写状态,如果不写默认为只读
AREA code, DATA,READONLY ;将数据段内存区域设置为只读状态,如果不写默认为可读可写
  1. 段对齐属性 ALIGN

    1
    AREA code, DATA,ALIGN=3  ;对齐数值范围为0~31

    该属性会使得该段的基地址进行相应的偏移,ALIGN=3表示基地址会在上一个段数据的基础之上偏移2^3=8个字节的位置

    1
    2
    3
    4
    5
    6
    7
    8
       	 AREA test1,DATA  ;假设这个段的基地址为0x00000100
    STR1 = "A"

    AREA test2,DATA ;ALIGN默认为2 那么这个段的基地址为0x00000104
    STR2 = "B"

    AREA test3,DATA,ALIGN=8 ;这个段的基地址为0x0000010C
    STR2 = "B"

    我们可以简单理解为,使用ALIGN这个属性可以让我们给上一个段预留除一部分缓冲区域,以ALIGN=2为例,当上一个段中的数据超过4个字节时,当前段基地器会向后再偏移4个字节,避免数据被覆盖,也就是说内存数据位置会进行重新分布,那么我们可以通过这个值来设置内存数据刷新频率,值越低,内存利用率越高,但是内存刷新频率也越高,负荷加重,反之,内存浪费越大,但是内存数据不需要频繁重新分布

    另外:

    除了在段属性中可以设置对齐之外,在指令中也可以插入ALGIN关键字:

    1
    2
    3
    4
    5
    6
    7
    8
    AREA code,CODE	   

    mov R0,R1

    ALIGN 4,3 ;下一条指令4字节对齐,并且偏移3个字节 为了补满4个字节,用0填充剩余1个未偏移位置
    mov R2,R0

    END

代码中使用AGLIN时用空格代替等号,同时单位为字节

多个代码段入口区分

当我们在同一个源文件中定义两个代码段时,程序从哪个段当做执行入口呢?

这个时候我们需要指定程序的入口,使用伪指令entry

1
2
3
4
5
6
7
8
9
10
 AREA test2,CODE 

mov R0,#7

AREA test3,CODE
entry ;程序入口
mov R0,#6
mov R1,#0x00000100
str R0,[R1]
END

栈的操作

  1. 入栈
1
2
3
4
5
push {R0}  ;将R0中的值存入栈内存中  相当于是STR R0,[R13,#-4]

入栈过程:
* 第一步:将栈顶指针往低地址偏移四个字节
* 第二步:将数据存入指针指向的内存空间
  1. 出栈
1
2
3
4
5
pop {R0}  ;将栈顶中的值取出存入R0寄存器 相当于是LDR R0,[R13],#0x0004

出栈过程:
* 第一步:将栈顶指针指向的内容取出存入寄存器
* 第二步:将指针往高地址恢复四个字节

pop和push 本质上使用的是LDR和STR内存读写指令

对栈批量操作

如果想批量操作多个连续栈空间的话,直接使用逗号分隔开,连续标号的寄存器使用横杠分隔

1
2
3
4
push {R0,R4-R12,LR} ;大括号中寄存器从右往左LR R12...R4 R0依次存入


pop {R0,R4-R12,PC} ;从左往右取出

除了使用pop和push之外,可以使用STM(store much)和LDM(load much)指令

格式:

1
STM 起始地址/基地址寄存器,{寄存器名称,多个寄存器以逗号或者-分割} ;起始地址寄存器R0-R14任意选择

示例:

1
2
3
4
5
6
7
8
9
STMFD  R13!,{R0,R4-R12,LR}    ;将寄存器列表中的寄存器(R0,R4到 R12,LR)存入栈,。
等价于
push {R0,R4-R12,LR} ;大括号中寄存器从右往左存入



LDMFD R13!,{R0,R4-R12,PC} ;将栈内容恢复到寄存器(R0,R4到R12,LR)。
等价于
pop {R0,R4-R12,PC} ;从左往右取出

pop和push,它们内部也是转成STM和LDM指令:

批量存取指令扩展

相关后缀含义:

  • IA:(Increase After):数据操作后基地址增4
  • IB:(Increase Before):数据操作前基地址增4
  • DA:(Decrease After):数据操作后基地址减4
  • DB:(Decrease Before):数据操作前基地址减4
  • FD: 满递减堆栈 (相当于STMDB+LDMIA)
  • FA: 满递增堆栈 (相当于STMIB+LDMDA)
  • ED: 空递减堆栈(相当于STMDA+LDMIB)
  • EA: 空递增堆栈 (相当于STMIA+LDMDB)

我们在使用的时候,要么使用结合的形式比如STMDB+LDMIA,要么直接使用封装形式比如STMFD+LDMFD

满栈和空栈

栈的生长方式可以有四种: 满增栈、满减栈、空增栈、空减栈

  • 满栈(full stack):栈指针指向下一个将要取出数据的位置,数据入栈时,栈顶指针先偏移再入栈,数据出栈是,先取数据,后指针偏移。
  • 空栈(empty stack):栈指针指向下一个将要放入数据的位置,数据入栈时,先存数据后指针偏移,数据出栈时,先指针偏移,后数据取出
  • 递增堆栈(ascending stack):堆栈由低地址向高地址生长。
  • 递减堆栈(secending stack):堆栈由高地址向低地址生长。

X86和mips架构都是采用满递减堆栈方式处理栈空间,ARM架构四种方式均支持

内存批量读写示例:

1
2
3
4
5
mov  R1,#4
mov R2,#5
mov R0,#0x00000008
stm R0,{R1,R2}
;以上四行代码表示 从0x00000008这个内存地址开始 将R1和R2中的数据依次存入

如果我想在上面的基础上再往后追加数据

1
2
3
mov  R1,#6
mov R2,#7
stm R0,{R1,R2}

我们发现数据并没有追加,而是被覆盖了,因为R0的值依然还是 0x00000008,这个时候我们需要使用扩展指令,如下:

1
2
3
4
5
6
7
8
mov  R1,#4
mov R2,#5
mov R0,#0x00000008
STMIA R0!,{R1,R2} ;只有在寄存器后加上!才能修改寄存器中的值

mov R1,#6
mov R2,#7
stm R0,{R1,R2}

事实上 STM指令如果不加后缀写法,默认使用的是STMIA指令,LDM指令默认使用LDMIA

多寄存器数据存放顺序

不管使用哪种扩展指令,皆为左低右高的形式,也就是左边的寄存器数据存放在低地址,右边的存放在高地址

1
2
3
4
STMIA  R0!,{R1,R2} ;左边R1的内容放低地址,右边R2的内容放高地址


LDMDB R0!,{R1,R2} ;高地址数据放入R2,低地址数据放入R1

宏

  1. 宏匹配

    • 语法格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    MACRO
    $label macroname $param
    ;指令序列
    MEND

    ;例如声明一个宏名为print的宏语句
    MACRO
    $label print $param ;带$的表示会被替换的内容
    ;这里写相关汇编代码
    MEND
    • 使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
     MACRO
    $label putR0 $param

    mov R0,$param

    MEND

    ; 使用
    putR0 #10
  • 延伸

第一个$label是干嘛用的呢,由于宏的内部处理方式的替换,为了避免标签名称的冲突,增加一个标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;假如我要在宏匹配中定义一个函数fun, 当我调用两次的时候,会出现函数名重复的问题
MACRO
$label putR0 $param
fun
mov R0,$param

MEND



putR0 #10
putR0 #10

;那么如果要解决这个问题的话,我可以利用第一个替换参数如下:
MACRO
$label putR0 $param
$label
mov R0,$param

MEND



fun1 putR0 #10 ;函数名为fun1
fun2 putR0 #10 ;函数名为fun2
  1. 宏定义

    • 全局宏的定义,可跨段访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;全局数字变量
    GBLA number
    number SETA 0Xaa

    ;全局逻辑变量
    GBLL flag
    flag SETL {TRUE}


    ;全局字符串变量
    GBLS str
    str SETS "hello world"
    • 局部宏的定义,只能在当前宏内被访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;局部数字变量
    LCLA number
    number SETA 0Xaa

    ;局部逻辑变量
    LCLL flag
    flag SETL {TRUE}


    ;局部字符串变量
    LCLS str
    str SETS "hello world"
    • 全局常量的定义
    1
    num   EQU      10  ;关键字EQU  数据不允许修改
  1. 宏定义示例

    1
    2
    3
    4
    5
    6
    7
    ;定义一个全局整型数据宏
    GBLA number
    number SETA 0Xaa

    ;使用
    mov R0,#number ;相当于mov R0,#0Xaa
    LDR R0,=number ;相当于LDR R0,=0Xaa

    局部宏数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    MACRO ;声明一个宏
    $label message $a ;取名为message 参数为$a

    ;定义宏内局部变量
    LCLA number
    number SETA $a

    mov R0,#number


    MEND ;宏结束,局部变量不再起作用


    message 10 ;直接调用

    常量数据

    1
    2
    3
    4
    5
    6
    7
    8
    	AREA data ,DATA
    num EQU 10
    AREA code ,CODE

    mov R1,#5
    ldr R1,=num

    END

宏匹配和宏定义需要遵循先定义后使用的原则

如果需要从宏中跳出,可以使用伪指令MEXIT

  1. 宏替换

    使用include或者get伪指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       ;使用get伪指令
    AREA code, CODE
    GET pangshu.s ;通知编译器当前源文件包含源文件 softool.s
    GET C:\pp.s ;通知编译器当前源文件包含源文件 C:\cn.s
    END

    ;使用include伪指令
    AREA code, CODE
    include pangshu.s ;通知编译器当前源文件包含源文件 softool.s
    include C:\pp.s ;通知编译器当前源文件包含源文件 C:\cn.s
    END

指令学习

传送指令

  1. 正常传送指令mov

    1
    mov R0,#4 ;将4传送至R0寄存器
  2. 取反传送指令mvn,也叫数据非传送指令

    1
    mvn R0,#4 ;将4取反后传送至R0寄存器  0100取反为1011

转移指令

  1. B指令

直接跳转,仅更改PC寄存器的值

示例:

1
2
3
B 0x00040000 ;直接跳转到物理地址0x00040000读取指令并执行

B 标号 ;直接跳转到标号处
  1. BL指令

跳转并链接,除了更改PC寄存器的值之外,还会将下一条指令所对应的物理地址存放至lr寄存器中

示例:

1
2
3
4
5
6
BL 0x00040000
mov r1,3 ;假设这行指令对应物理地址为0x0040004, 那么BL一旦执行,会将该值存入lr寄存器


或者
BL 标号
  1. BX指令

    跳转并切换状态

    BX指令后面只能跟寄存器,弥补了B指令和BL指令的不足, 同时会根据寄存器中最低比特位值切换ARM/Thumb模式

示例:

1
2
3
4
5
6
BL print


print
mov r1,#1
BX lr ;函数返回 如果R0[0]=1,则进入Thumb状态 反之进入ARM模式

除了通过指令来更改PC寄存器值之外,在ARM32中还可以直接使用传送指令对PC寄存器进行赋值:

1
2
3
mov pc,#0x00000008  ;往pc寄存器中写入一个地址值

mov R0,pc ;获取pc寄存器中的值
  1. BLX指令

    该指令将以下功能集于一身

    • 更改PC寄存器的值
    • 将下一条指令的地址存入lr寄存器
    • 根据寄存器中最低比特位值切换ARM/Thumb模式

算术和逻辑运算指令

  1. 算术运算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    add R0,#5
    add R0,R1 ;加法


    sub R0,#5
    sub R0,R1 ;减法

    mul R0,R1,R2 ;乘法指令, 这里至少三个寄存器参与

    mla R0,R1,R2,R3 ;先乘后加 R0 = R1 × R2 + R3
  2. 逻辑运算

    1
    2
    3
    4
    5
    6
    7
    8
    and R0.#3
    and R0,R0 ;逻辑与

    orr R0.#3
    orr R0,R0 ;逻辑或

    eor R0.#3
    eor R0,R0 ;逻辑异或

移位指令

1
2
3
MOV   R0, R1, LSL#2    ;将R1中的内容左移两位后传送到R0中。

MOV R0, R1, LSR#2 ;将R1中的内容右移两位后传送到R0中,左端用零来填充。

比较指令

  1. 比较两个值是否相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cmp R0,R1 
    beq sub ;如果两个寄存器中的值相等则跳转到sub函数中,否则继续往下执行

    sub


    cmp R0,#5
    bne sub ;如果两个值不相等,跳转到sub函数,否则继续往下执行

    sub
  2. 大于和小于(带符号)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ;大于
    cmp R0,R1
    bgt sub ;如果R0寄存器中的值大于R1,则跳转至sub

    sub

    ;小于
    cmp R0,R1
    blt sub ;如果R0寄存器中的值大于R1,则跳转至sub

    sub

标志寄存器

试想一下,我们的比较指令cmp,它内部是如何进行数据大小判断的

在高级语言里,直接使用>或者<运算符,来判断两个值的大小,比较结束后返回True或者Flase,可是在汇编语言里面没有这么简便,那它又是如何对两个数据之间大小进行判断的呢?

别忘了, 计算机最擅长做二进制的算术和逻辑运算

1
cmp R0,R1

要想判断两个数据是否相等,或者大于小于,直接做个减法运算不就完事了,也就是R0-R1,如果结果为0,那么两个值相等,如果结果为正数,则R0>R1,结果为负数,则小于

但是问题来了,这个结果值放在哪里呢?放内存中还是寄存器呢? 答案是:寄存器

cpu设计者为了方便区分专门用了一个寄存器来存放数据运算后的结果,这个寄存器叫做状态寄存器,也叫标志寄存器

ARM32中一个寄存器有32二进制位的数据空间,那该怎么存放呢?

1
00000000000000000000000000000000  ;32个二进制位

为了方便程序员开发,设计者给这些二进制位进行了相应的命名:

当两个比较值相等,进行减法运算时,结果为0,那么Z标志位的值为1,也就是

1
01000000000000000000000000000000  ;32个二进制位

如果不相等,结果不为0,那么Z标志位的值变成0

由于每个二进制位只能存0和1两个值,也就是最多只能表示两种状态,那大于和小于的状态表示就得放到另外一个二进制上了,由于二进制运算涉及到有符号和无符号两种情况,因此需要用到两个二进制分别进行处理,有符号的的结果存放在N标志位,无符号的结果存放在C标志位:

cmp指令会同时对两个数据进行有符号和无符号运算

有符号运算,如果结果为正数,N标志位值为0,如果为负数,N标志位值为1

无符号运算,如果结果为正数,C标志位值为1,如果为负数,C标志位值为0

那么我们在使用cmp指令的时候,到底是根据那个标志位的结果进行判断的呢?

如果我们使用bne指令,那么取Z标志位的值进行参考

如果我们使用blt,bgt,那么取N标志位,Z标志位和V标志位三者的值进行参考

总结:

  1. cmp指令的功能相当于减法指令,只是对操作数之间运算比较,结果间接保存在标志寄存器高位中

  2. bne,blt,bgt等这些指令都是通过获取标志寄存器中的值来得知比较结果从而进行相应跳转,不同的指令需要满足不同的条件

  3. 我们可以通过改变状态寄存器中的值来改变代码的走向

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AREA test,CODE

mov R0,#5
mov R1,#6
cmp R0,R1
;在跳转之前改变状态寄存器的值 使得bgt必然跳转
MSR cpsr_f ,0x20000000
BGT fun
mov R1,#6

fun
mov R0,#4
bx lr


END

知识扩展:

  1. 状态寄存器的读取和写入

    读取指令:MRS{mov to register from special register)

    写入指令: MSR(Move to Special register from Register )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 读取状态寄存器中的值
    mrs R0,cpsr ;将值读取到R0寄存器


    #修改状态寄存器中的值
    msr cpsr_c,#0x2d ;修改控制位区域
    msr cpsr_x,#0x2d00 ;修改扩展位区域
    msr cpsr_s,#0x2d0000 ;修改状态位区域
    msr cpsr_f,#0x2d000000 ;修改标志位区域

    #修改状态寄存器高位值时,低位必须补足0,虽然加了0但是不会影响其他区域的值
  2. 状态寄存器中各个区域具体描述

    一共分位四个大区域,从低到高分别为:控制位区域, 扩展位区域, 状态位区域, 标志位区域,每个区域各占8个二进制位的空间

    以下是控制位区域细分详解图:

  1. 比较指令标志位条件参考表
指令 含义 需要满足的条件
beq 相等 Z标志位为1
bne 不相等 Z标志位为0
bgt 带符号大于 Z标志位为0,且N和V标志位值相等
blt 带符号小于 N不等于V
bge 带符号大于等于 N等于V
ble 带符号小于等于 Z标志位为1或者N不等于V
bls 无符号小于等于 Z标志位为1且C标志位为0
bhi 无符号大于 Z标志位为0且C标志位为1
bcs 无符号大于等于 C标志位为1
bhs 无符号大于等于 C标志位为1
bcc 无符号小于 C标志位为0
blo 无符号小于 C标志位为0
bmi 负数 N标志位为1
bpl 正数或零 N标志位为0
bvs 溢出 V标志位为1
bvc 未溢出 V标志位为0
bnv 无条件执行 忽略
bal 无条件执行 忽略

条件和循环伪指令

IF、ELSE 和 ENDIF

  • 根据条件的成立与否决定是否执行某个程序段

  • IF、ELSE、ENDIF 伪指令可以嵌套使用

  • 1
    2
    3
    4
    5
    6
    7
    8
    GBLL Test ;声明一个全局逻辑变量Test
    Test SETL {TRUE}

    IF Test = {TRUE}
    程序段1
    ELSE
    程序段2
    ENDIF

WHILE 和 WEND

  • 根据条件的成立与否决定是否重复汇编一个程序段

  • 若 WHILE 后面的逻辑表达式为真,则重复汇编该程序段,直到逻辑表达式为假

  • WHILE 和 WEND 伪指令可以嵌套使用

  • 1
    2
    3
    4
    5
    6
    GBLA Counter  ;声明一个全局数字变量Counter
    Counter SETA 3 ;赋值
    ...
    WHILE Counter < 10
    程序段
    WEND

汇编语言和C语言交互

  • 内嵌汇编
  • 外链汇编

1.引入其他源文件函数

使用import或者extern伪指令

1
2
3
4
5
6
7
8
9
;使用import伪指令
AREA code, CODE
import fun1 ;导入其他源文件中名为fun1的函数
END

;使用extern伪指令
AREA code, CODE
extern fun1
END

两者区别:

  • import:不管当前文件是否使用该引入的函数,该标签都会加入当前文件符号表,即为静态引用
  • extern:只有当前文件使用了该函数,才会将此标签加入符号表,即为动态引用

2.导出当前源文件中函数供其他文件访问

使用export或者global伪指令

1
2
3
4
5
6
7
8
;使用import伪指令
AREA code, CODE
export fun ;导出fun函数供其他源文件使用

fun
mov R0,#4
bx lr
END

3.外链汇编之C语言调汇编函数

第一步,在汇编原文件中将函数暴露出来给供外部调用,使用export或者global伪指令:

1
2
3
4
5
6
7
8
9
10
11
12
AREA code, CODE
export arm_strcpy ;或者使用global

arm_strcpy
loop
ldrb R4,[R0],#1 ;如果使用ldr 那么将偏移值改成4
cmp R4,#0
beq over
strb R4,[R1],#1
b loop
over
END

第二步,在C文件中引用汇编中的函数,C文件中只能使用extern伪指令:

1
2
3
4
5
6
7
extern arm_strcpy(char *src,char*des);

int main2(){
char *a="hello pangshu";
char b[64];
arm_strcpy(a,b);
}

4.外链汇编之汇编调c语言函数

第一步,在C文件中编写好函数

1
2
3
int c_sum(int a,int b){
return a+b;
}

第二步, 在汇编文件中引入函数,使用import或者extern伪指令

1
2
3
4
5
6
7
AREA code, CODE
import c_sum

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数

END

第三步, 使用BL指令调用函数

1
2
3
4
5
6
7
AREA code, CODE
import c_sum

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数
BL c_sum
END

在ARM中函数参数使用R0~R3这四个寄存器来进行传递,最多传递4个参数,超过4个参数使用栈进行处理,函数返回值通过R0进行传递

由于keil软件的特殊性,我们可以通过以下方式进行互调测试

C文件中代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
extern arm_strcpy(char *src,char*des);

int main2(){

char *a="hello pangshu" ;

char b[64];

arm_strcpy(a,b); //调汇编中函数
return 0;
}

int c_sum(int a,int b){
return a+b;
}

汇编文件中代码:

1
2
3
4
5
6
7
8
9
10
11
 AREA code, CODE

import c_sum
export arm_strcpy

arm_strcpy

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数
BL c_sum ;结果存放至R0中
END

5.内嵌汇编

在C语言中嵌入汇编代码,格式如下:

1
2
3
4
5
6
7
8
9
10
11
int main2(){
int a=4;
int b=4;
int c=4;

__asm__{ //使用__asm或者__asm__
mov R5,#0x00000005 //在大括号内部直接写入汇编代码即可
mov R6,#0x00000005
}
return 0;
}

内嵌汇编的注意事项:

  • 不能直接给PC寄存器赋值,如果想改变pc值需要借助转移指令
  • 由于R0R3用于存放函数参数和返回值,R12R15有特殊用途,因此我们能操作的寄存器只有R4~R11, 又因为编译器会优先将寄存器分配给函数中的局部变量,因此我们一般无法在内嵌汇编环境中准确地修改某个寄存器的值,比如我想修改R5寄存器的值,由于函数有个变量占用了R5这个寄存器,那么编译器会自动将你写的这个R5改成R6或者其他,所以,在内嵌汇编时我们需要把寄存器当作变量来看待,把局部变量也当成寄存器看待,就好理解了
1
2
3
4
5
6
7
8
9
10
11
void c_strcopy(char *src,char *des){
char ch
__asm__{
loop:
ldrb ch,[src],#1 //局部变量当成寄存器看待
strb ch,[des],#1
cmp,ch,#0
bne loop
}

}

ARM32中寄存器别名补充

寄存器 别名 用途
r0 a1 第一个函数参数和函数返回值
r1 a2 第二个函数参数
r2 a3 第三个函数参数
r3 a4 第四个函数参数
r4 v1 寄存器变量
r5 v2 寄存器变量
r6 v3 寄存器变量
r7 v4 寄存器变量
r8 v5 寄存器变量
r9 v6 寄存器变量 实际的帧指针
r10 sl 栈接线
r11 fp 参数指针
r12 ip 临时
r13 sp 栈指针
r14 lr 连接寄存器
r15 pc 程序计数器

如何编译16位arm汇编指令

1
2
3
4
AREA test, CODE
code16 ;声明为16位arm指令 如果不写默认则为code32

END

附:指令集汇总

(一) ARM 指令集

1. 指令格式

2. 条件码

3. ARM 存储器访问指令

1) LDR/ STR -加载 /存储指令

2) LDM/ STM -多寄存器加载 /存储指令

3) SWP -寄存器和存储器交换指令

4. ARM 数据处理指令

1) 数据传送指令

a) MOV -数据传送指令

b) MVN -数据非传送指令

2) 算术逻辑运算指令

a) ADD -加法运算指令

b) SUB -减法运算指令

c) RSB- 逆向减法指令

d) ADC -带进位加法指令

e) SBC -带进位减法指令

f) RSC -带进位逆向减法指令

g) AND -逻辑“与”

h) ORR -逻辑“或”

i) EOR -逻辑“异或”

j) BIC -位清除指令

3) 比较指令

a) CMP -比较指令

b) CMN -负数比较指令

c) TST -位测试指令

d) TEQ -相等测试指令

4) 乘法指令

a) MUL - 32位乘法指令

b) MLA - 32位乘加指令

c) UMULL - 64位无符号乘法指令

d) UMLAL - 64位无符号乘加指令

e) SMULL - 64位有符号乘法指令

f) SMLAL - 64位有符号乘加指令

5. ARM 分支指令

1) B -分支指令

2) BL -带连接的分支指令

3) BX -带状态切换的分支指令

6. ARM 协处理器指令

1) CDP -协处理器数据操作指令

2) LDC -协处理器数据读取指令

3) STC -协处理器数据写入指令

4) MCR - ARM处理器到协处理器的数据传送指令

5) MRC -协处理器到 ARM处理器的数据传送指令

7. ARM 杂项指令

1) SWI -软中断指令

2) MRS -读状态寄存器指令

3) MSR -写状态寄存器指令

8. ARM 伪指令

1) ADR -小范围的地址读取伪指令

2) ADRL -中等范围的地址读取伪指令

3) LDR -大范围的地址读取伪指令

4) NOP -空操作伪指令

(二) Thumb 指令集

1. Thumb 指令集和 ARM指令集的区别

2. Thumb 存储器访问指令

1) LDR/ STR -加载 /存储指令

2) PUSH/ POP -寄存器入栈 /出栈指令

3) LDMIA/ STMIA -多寄存器加载 /存储指令

3. Thumb 数据处理指令

1) 数据传送指令

a) MOV -数据传送指令

b) MVN -数据非传送指令

c) NEG -数据取负指令

2) 算术逻辑运算指令

a) ADD -加法运算指令

b) SUB -减法运算指令

c) ADC -带进位加法指令

d) SBC -带进位减法指令

e) MUL -乘法运算指令

f) AND -逻辑“与”

g) ORR -逻辑“或”

h) EOR -逻辑“异或”

i) BIC -位清除指令

j) ASR -算术右移指令

k) LSL -逻辑左移指令

l) LSR -逻辑右移指令

m) ROR -循环右移指令

3) 比较指令

a) CMP -比较指令

b) CMN -负数比较指令

c) TST -位测试指令

4. Thumb 分支指令

1) B -分支指令

2) BL -带连接的分支指令

3) BX -带状态切换的分支指令

5. Thumb 杂项指令

1) SWI -软中断指令

6. Thumb 伪指令

1) ADR -小范围的地址读取伪指令

2) LDR -大范围的地址读取伪指令

3) NOP -空操作伪指令

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

1…262728…48

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

479 日志
139 标签
RSS
© 2024 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%