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

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


  • 首页

  • 归档

  • 搜索

GCC工具链都包含哪些工具

发表于 2021-01-27

GCC

GCC原名为GNU C语言编译器(GNU C Compiler),原本只能处理C语言。后来随着功能的扩展,支持的语言种类越来越多 ,故更名为GCC(GNU Compiler Collection,GNU编译器套件)

因此 GCC既可以指代C编译器,也可以指代GNU编译套件

其套件囊括了许多子工具:

  • gcc: C 编译器

  • g++: C++ 编译器

  • cpp: C 预处理器

  • as: 汇编器

  • ld: 连接器

  • objcopy: 目标文件翻译器,用于从连接器输出中创建一个ROM 映像

  • objdump: 目标文件阅读器, 用于反汇编目标文件

  • make: make 工具

  • gdb: 源代码调试器

GCC工具链

一个程序从代码编译到机器执行,中间需要经历很多步骤,比如从预编译,编译,到汇编和连接, 这一系列环环相扣过程中涉及到的GNU工具集,称之为GCC工具链

在windows平台中,我们有许多的图形化IDE可以选择,一般编译工具链都集成进了软件内部,无需开发者关心,但是在Linux平台,基本以命令行的方式进行操作,那么对于开发者来说,需要了解每个工具的作用和具体使用方法

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

img

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

汇编语言之GNU ARM

发表于 2021-01-25

什么是GNU

GNU最开始其实是一个操作系统,旨为打造一个开源免费自由的操作系统,目前操作系统还在完善中

GNU计划: 最初目标是创建一套完全自由的操作系统GNU 和相应的软件

GCC :(GNU Compiler Collection)GNU编译器套件,GNU提供的一整套的工具集,这套工具集中包含了汇编器,编译器和链接器,二进制转换,调试工具等

GCC优势:

  • 免费开源
  • 贴近系统底层,功能强大,灵活性高
  • 跨平台,方便交叉编译

GCC劣势:

  • 工具基本采用命令行方式,学习和使用门槛较高

接下来我们要学习的就是GNU计划众多的产物之一GNU FOR ARM

汇编器与指令集

  1. 什么是汇编器

    将汇编语言翻译成机器码的工具

  2. 什么是编译器

    将高级语言翻译成机器语言或者汇编语言的工具

  3. 汇编器和编译器的区别

    汇编器的服务对象是汇编语言,编译器的服务对象是高级语言

  4. 汇编器和汇编语法伪指令的关系

    不同的CPU对应不同的指令集 ,不同的汇编器对应不同的伪指令集和汇编语法。

    每种汇编器都可以有自己的伪指令集和自己的语法

使用不同的汇编器汇编同一个cpu架构的汇编代码,所对应的指令绝对是一致的,但伪指令各有千秋

1
2
3
4
5
6
7
8
9
;使用ARM官方的汇编器
AREA test, CODE
mov R3,#5
END

;使用GNU的汇编器
.text ;伪指令
mov R3,#5 ;传送指令皆为mov
.end

常见的汇编器

  • MASM汇编器:微软旗下专为x86架构打造的一款汇编器,支持8086汇编和win32汇编
  • GNU汇编器 : 简称为GAS,是GNU旗下的一款免费开源跨平台汇编器其子集中包含了支持多种架构的汇编器,比如GNU FOR ARM就是单独面向ARM架构的汇编器,此外还有GNU FOR X86等
  • NASM汇编器: 是一款面向x86架构的汇编器,支持8086汇编和win32汇编,同时可跨平台, 免费开源
  • ARMASM汇编器:ARM官方原生的汇编器,集成在了ADS工具上,适用于ARM架构,我们也一般称之为ADS汇编器

两种ARM汇编器的各自用途

  • ARMASM汇编器:一般用于windows平台
  • GAS汇编器:支持windows平台和linux平台,方便跨平台交叉编译

由于移动设备如安卓和iphone底层都是采用GNU的编译环境,我们如果要进行移动端的开发,那么势必需要掌握GNU ARM, 同时和ADS和KEIL收费工具相比,GUN工具全部免费,方便开发者进行使用

如果你是从事android开发,有兴趣可以去翻NDK r17以下版本的库,里面用的编译工具就是GCC

GNU ARM开发环境搭建

我们需要准备以下两个工具:

  1. GCC编译套件
  2. 安卓模拟器

GCC编译套件根据cpu架构和操作系统的不同,又分为了很多子类:

  • 纯ARM裸机: 对应arm-none-eabi工具包
  • ARM架构+Linux操作系统:对应arm-none-linux-eabi工具包

由于接下来我们选择在安卓模拟器上进行开发学习,因此我们选择arm-none-linux-eabi这套工具来进行代码的编译

工具下载

GCC工具的具体使用

伪指令和伪操作

  1. 注释

    1
    @我是注释
  2. 段的声明

    • 代码段

      1
      2
      .text
      @代码
    • 数据段

      1
      2
      .data
      @代码
  • 自定义一个段

    1
    .section .pangshu @定义一个名为.panghsu的段
  1. 函数或者标签的声明

    1
    2
    3
    fun:  @在GNU环境中标签后面需要加冒号,而原生环境则不用
    mov R0,#4
    bx lr
  2. 数据的声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .byte @定义单字节数据,例如:.byte 0x14;
    .short @定义双字节数据,例如:.short 0x1000;
    .long @定义四字节数据,例如:.long 0x10001000;
    .quad @定义8字节,如:.quad 0x1234567890abcd;
    .float @定义浮点数,如:.float 0f-314159265358979323846264338327/95028841971.693993751E-40 @ -pi

    @字符串定义
    .string “abcd”, “efgh”, “hello!”
    .asciz “qwer”, “sun”, “world!”
    .ascii “welcome/0”
    @需要注意的是:.ascii伪操作定义的字符串需要自行添加结尾字符’/0’。
  3. 数据的批量定义

    • 格式如下:

      1
      2
      3
      .rept @重复次数
      @数据定义代码
      .endr @结束重复定义
    • 示例

      1
      2
      3
      .rept 3
      .byte 0x23
      .endr
  4. 关于align

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    .global _start
    _start:
    mov r0, #0x12

    .byte 0x12
    .byte 0x34

    ;.align 后跟为0,1,2或者不跟参数时都当作是4字节对齐
    .align
    .byte 0x56

    ; 2^3次方,8字节对齐
    .align 3
    .word 0x1

    ; 2^5次访,32字节对齐
    .align 5
    .word 0x2

    ; 填充0x12345678,直到16字节对齐
    .balignl 16,0x12345678

    ;字符串
    .ascii "abc123"

    .align
    ;.ascii和.asciz的区别是,.asciz会在字符串后自动添加结束符\0.
    .asciz "def456"

    mov r0, #0xab

    ;.balign[wl] align, fill_value, max_padding
    .align
    ; .balign 后跟的align参数必须为2的次方,否则会报错,按字节填充, fill_value要为字节
    .balign 8, 0x12

    ; .balignw 按2字节填充
    .balignw 8, 0x3456

    ; .balignl 按4字节填充
    .balignl 8, 0xabcdef01

    反汇编后的结果:

    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
    26
    27
    28
    29
    00000000 <_start>:
    0: e3a00012 mov r0, #18
    4: 3412 .short 0x3412 // .byte 0x12 和 .byte 0x34
    6: 0000 .short 0x0000 // .align 引起的填充
    8: 00000056 andeq r0, r0, r6, asr r0 // .byte 0x56
    c: e1a00000 nop ; (mov r0, r0) // .align 3 8字节对齐
    10: 00000001 .word 0x00000001 // .word 0x1
    14: e1a00000 nop ; (mov r0, r0) // .align 5 ,32字节对齐
    18: e1a00000 nop ; (mov r0, r0)
    1c: e1a00000 nop ; (mov r0, r0)
    20: 00000002 .word 0x00000002 // .word 0x2
    24: 12345678 .word 0x12345678 // .balignl 16,0x12345678,填充0x12345678,直到16字节对齐
    28: 12345678 .word 0x12345678
    2c: 12345678 .word 0x12345678
    30: 31636261 .word 0x31636261 // .ascii "abc123"
    34: 3332 .short 0x3332
    36: 0000 .short 0x0000
    38: 34666564 .word 0x34666564 // .asciz "def456"
    3c: 3635 .short 0x3635
    3e: 00 .byte 0x00
    3f: e3a000ab mov r0, #171 ; 0xab // mov r0, #0xab, 这里就不对齐了
    43: 00 .byte 0x00
    44: 12121212 .word 0x12121212 // .balign 8, 0x12
    48: e1a00000 nop ; (mov r0, r0)
    4c: e1a00000 nop ; (mov r0, r0)
    50: e1a00000 nop ; (mov r0, r0)
    54: e1a00000 nop ; (mov r0, r0)
    58: e1a00000 nop ; (mov r0, r0)
    5c: e1a00000 nop ; (mov r0, r0)

指令和伪指令的区别

  • 指令: 有与之对应的机器码,能被cpu所识别,和编译器无关

  • 伪指令:没有与之对应的机器码,无法被cpu识别,只能被编译器识别,不同编译器伪指令不一样

    不同的CPU对应不同的指令集;不同的汇编器对应不同的语法和伪指令集

例子:ARM原生编译器和GNU FOR ARM

两种汇编器语法对比一览表

GNU ARM汇编 ADS ARM汇编
“@”或“/…/” “;”
.include GET
.equ EQU
.global EXPORT
.extern IMPORT
.long DCD
.end END
entry: ENTRY
.text AREA Init,CODE,READONLY
.data AREA Block,DATA,READWRITE
.macro MACRO
.endm MEND

汇编语言和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进行传递

5.内嵌汇编

GNU内嵌汇编,格式如下:

1
2
3
4
5
6
7
int main2(){
__asm__( //大括号改成中括号
"mov R5,#0x00000005\n" //汇编指令需要使用引号包裹,多条语句之间使用回车换行符进行分隔
"mov R6,#0x00000005"
); //需要以分号结尾
return 0;
}

学习工具

  • 在线ARM汇编编辑器:https://azm.azerialabs.com/

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

img

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

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

发表于 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

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

关于error establishing a database connection

发表于 2021-01-22

起因

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

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

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

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

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

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

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

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

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

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

解决方案

对表进行修复即可:

  1. 执行修复指令

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

    1
    check table wp_option

修复前最好先备份数据库

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

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

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

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

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

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

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

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

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

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

视频教程录制计划

发表于 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. 《音频插件开发视频教程-胖薯出品》

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

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

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

硬件开发之个人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

总结

在串联电路中

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

在并联电路中

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

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

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

汇编语言之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=3 ;这个段的基地址为0x00000108
    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

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

  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
    GBLL Test ;声明一个全局逻辑变量Test
    ...
    IF Test = TRUE
    程序段1
    ELSE
    程序段2
    ENDIF1234567

WHILE 和 WEND

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

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

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

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

汇编语言和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
int main2(){
__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处理器的寻址方式

二、指令集学习

(一) 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 -空操作伪指令

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

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

123…23

乱码三千

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

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