qqCPUID 的一个汇编实例

本文详细介绍了使用汇编语言编写、编译、连接及调试CPUID指令的过程,重点阐述了如何通过GCC与GDB实现程序的调试,并分析了使用-gstabs参数对程序大小的影响。此外,文章还展示了如何利用C库函数(如printf)简化程序开发过程,以及静态连接与动态连接的区别和应用场景。
qqCPUID 的一个汇编实例
2010年09月04日
  使用 CPUID 查看 CPUID 生成的厂商 ID 字符串,目的是了解汇编程序编写的基本框架,编译,连接,调试。程序代码:
  #cpuid.s Sample program to extract the processor Vendor ID
  .section.data
  output:
  .ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"
  .section.text
  .global _start
  _start:
  movl$2,%eax
  cpuid
  movl $output,%edi
  movl %ebx, 28(%edi)
  movl %edx, 32(%edi)
  movl %ecx, 36(%edi)
  movl $4, %eax
  movl $1, %ebx
  movl $output,%ecx
  movl $42, %edx
  int $0x80
  movl $1, %eax
  movl $0, %ebx
  int $0x80
  , 因为 gcc 超找的标签是 main ,而 as 查找的是 _start 。改了之后,可以如下编译并生成可执行文件:
  使用 gdb 来调试程序
  使用 gdb 调试汇编程序,首先必须使用 -gstabs 参数重新编译汇编源代码:
  需要注意的是,调试完程序后,要重新用不使用 -gstabs 参数的方式再编译生成程序。因为有了 -gstabs 参数,程序里会添加进调试信息,整个程序会变得庞大,如上面的程序使用 -gstabs 和不使用 -gstabs 选项所生成的程序大小差别为:
  可见,用的比不用的差不多比不用的大了它的 1/2 大小。假如程序越大,那么产生的调试信息也就越多。
  经过 -gstabs 编译后,现在可以用 gdb 进行调试了:
  beyes@beyes-groad:~/programming/assembly/cpuid$ gdb cpuid
  GNU gdb (GDB) 7.0-ubuntu
  Copyright (C) 2009 Free Software Foundation, Inc.
  License GPLv3+: GNU GPL version 3 or later
  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 "i486-linux-gnu".
  For bug reporting instructions, please see:
  ...
  Reading symbols from /home/beyes/programming/assembly/cpuid/cpuid...don e.
  (gdb)
  ),然后再使用 run 命令,但会看到程序不会在断点处停下,而是直接运行完整个程序。可以说,这是一个长期的缺陷,也可能不算是一个缺陷。解决这个问题的办法是,在 _start 标签后面加一条 NOP 指令,如:
  后面,就可以使用 next 或者 step 来跟踪调试。
  要查看相关的数据内容,经常会使用到以下 3 个命令:
  数据命令
  描述
  info registers
  显示所有寄存器的值
  print
  显示特定寄存器或来自程序的变量的值
  x
  显示特定内存位置的内容
  (gdb) info registers
  eax 0xa 10
  ecx 0x6c65746e 1818588270
  edx 0x49656e69 1231384169
  ebx 0x756e6547 1970169159
  esp 0xbffff480 0xbffff480
  ebp 0x0 0x0
  esi 0x0 0
  edi 0x80490ac 134516908
  eip 0x8048081 0x8048081
  eflags 0x212 [ AF IF ]
  cs 0x73 115
  ss 0x7b 123
  ds 0x7b 123
  es 0x7b 123
  fs 0x0 0
  gs 0x0 0
  print 命令也可以用于显示各个寄存器的值,还可以在 print 后加一个修饰符就可以修改 print 命令输出格式:
  x 命令用于显示特定内存位置的值。和 print 命令类似,可以使用修饰符修改 x 命令的输出。x 命令的格式是:
  其中,n 是要显示的字段数,y 是输出格式,它可以是:
  (gdb) x/42cb &output
  0x80490ac : 84 'T' 104 'h' 101 'e' 32 ' ' 112 'p' 114 'r' 111 'o' 99 'c'
  0x80490b4 : 101 'e' 115 's' 115 's' 111 'o' 114 'r' 32 ' ' 86 'V' 101 'e'
  0x80490bc : 110 'n' 100 'd' 111 'o' 114 'r' 32 ' ' 73 'I' 68 'D' 32 ' '
  0x80490c4 : 105 'i' 115 's' 32 ' ' 39 '\'' 71 'G' 101 'e' 110 'n' 117 'u'
  0x80490cc : 105 'i' 110 'n' 101 'e' 73 'I' 110 'n' 116 't' 101 'e' 108 'l'
  0x80490d4 : 39 '\'' 10 '\n'
  上面,42 表示显示 output 变量 (&符号用于表明它是一个内存位置) 的前 42 个字节;c 表示一次显字符;b 表示一次显示一个字节。在输出的内容里,字符前面的数字是这个字符的 ASCII 码。当跟踪对内存位置进行操作的指令时,这个命令的特性价值无法衡量。
  movl $0, %eax
  给 eax 送入 0 值,目的在于这个输入可以使 cpuid 输出厂商 ID 字符串。
  cpuid
  执行 CPUID 指令。
  执行 cpuid 指令后,就要用 3 个输出寄存器 ebx, edx, ecx 来收集指令的相应信息:
  movl $output, %edi
  movl %ebx, 28(%edi)
  movl %edx, 32(%edi)
  movl %ecx, 36(%edi)
  上面的28, 32, 36 对应的是 "The processor Vendor ID is 'xxxxxxxxxxxx'\n" 中的 'xxxxxxxxxxxx 的偏移位置。
  movl $output, %edi 是创建了一个指针 edi ,output 标签的内存位置被加载到 EDI 中。这里需要注意,EBX, EDX, ECX 的顺序,这个顺序并不是按照 B,C,D 来排序,比较奇怪。
  经过上面的几条指令后,就在内存中放置好厂商的 ID 字符串了,下面的几条指令用来显示这些信息:
  movl $4, %eax
  movl $1, %ebx
  movl $output, %ecx
  movl $42, %edx
  int $0x80
  第一条指令,把 4 放到 eax 寄存器中。这个 4 表示系统调用值。在较新的内核中,这些系统调用定义在:
  arch/x86/include/asm/unistd_32.h
  文件中。比如 4 号系统调用是:
  这里,每个系统调用都被定义为一个名称 (前面加上 __NR_) ,和它的系统调用号。
  第二条指令,把 1 放到 ebx 寄存器中。这里的 1 是文件描述符,表示的标准输出 (STDOUT) .
  所以,第 1 ,第 2 条指令合起来的意思是,调用 write() 系统调用,往标准输出上输出要显示的内容。
  第三条指令,是字符串的开头送往 ecx 中。
  第四条指令,是把字符串的长度送往 edx 中。
  对比 write() 函数的原型:
  write(int fd, const void *buf, size_t count);
  从左到右,3 个参数分别对应着 ebx, ecx, edx 这 3 个寄存器中的值。可见:
  EAX 包含系统调用值
  EBX 包含要写入的文件描述符
  ECX 包含字符串的开头
  EDX 包含字符串的长度
  最后,在显示了厂商的 ID 信息之后,就干净的退出程序。同样,Linux 系统调用可以为此提供帮助,这时使用系统 1 号调用 (#define __NR_exit 1) ,程序被正确的终止,并且返回到命令提示符。在使用 exit() 时,exit() 也有一个作为退出代码的参数,这个参数装载 ebx 寄存器中。
  Linux 内核提供了许多可以很容易地从汇编应用程序访问的预置函数(如系统调用),为了访问这些内核函数,必须使用 int 指令码,它生成具有 0x80 值的软件中断。执行的具体函数由 EAX 寄存器中的值来确定。如果没有这个内核函数,就必须自己把每个输出字符发送到正确的显示器 I/O 地址,这无疑要花费很多的时间。
  在汇编中使用 C 库函数,对程序开发达到简洁省时的目的,代码如下:
  #cpuid2.s View the CPUID Vendor ID string using C library calls
  .section.data
  output:
  .asciz"The processor Vendor ID is '%s'\n"
  .section.bss
  .lcommbuffer,12
  .section.text
  .global_start
  _start:
  movl$0,%eax
  cpuid
  movl$buffer,%edi
  movl%ebx, (%edi)
  movl%edx,4(%edi)
  movl%ecx,8(%edi)
  pushl$buffer
  pushl$output
  callprintf
  addl$8,%esp
  pushl$0
  callexit
  在程序中,使用了 C 库中函数 printf 。如果直接编译连接,那么编译通过,而连接时则会出错:
  为了连接 C 库,首先要确保这个 C 库在系统上可用。把 C 函数连接到汇编程序有两种方法。
  第一种是静态连接 ( static linking ) 。静态连接把函数的目标代码直接连接到应用程序的可执行程序文件中,这样创建的可执行文件会很大;而且,如果同时运行程序的多个实例,就会造成内存浪费( 每个实例都有其自己的相同函数的拷贝 )。
  第二种是动态连接 ( dynamic linking ) 。动态连接使用库的方式在程序运行时,由操作系统调用动态链接库,并且多个程序可以共享动态链接库。标准的 C 动态库位于 libc.so.x 文件中。我的系统是 ubuntu 9.10,所使用的是 libc.so.6 版本,其实这是一个软连接,指向真实的库名是 libc-2.10.1.so 。
  如果程序使用 gcc 来编译(在程序中 _start 改成 main),则文件会自动连接到 C 程序。
  在用 ld 连接目标文件时,为了连接 libc.so 文件,必须使用 -l 参数,在指定库时,并不需要指定完整的库名称:
  上面,-l 选项后面紧接着一个 c 。c 表示 /lib/libc ,c 的前面部分 /lib/lib 是所有函数动态库的默认命名前缀。
  经过上面的连接后,生成了可执行文件,但这个可执行文件却不能正确执行:
  由上可见,现在可以正常执行程序了。
  使用 gcc 编译器时,会自动连接必须的 C 库,无须进行任何特殊的操作,但 gcc 是静态连接;而在 ld 里指定 -dynamic-linker 参数则是动态连接,动态连接是在运行时加载,所以最后产生的可执行文件比较小,如:
  或者用 objdump -d cpuid2 命令来查看 dump 出的文件信息,也可以明显看到 gcc 编译生成的文件里有很多内容。
  程序说明:
  调用 printf 函数,事先要把 printf() 函数参数压入堆栈,参数入栈的顺序是从右到左。
非常好用的SQLiteSpy 1.8.16 免费正式版(内附有一做好的DB,大家可以马上看下效果)。网上看到的,整理了下,共享给大家! 希望有用! SQLiteSpy是一个快速和紧凑的图形用户界面的SQLite数据库管理软件。它可以读取sqlite3文件并执行SQL。图形用户界面使得它很容易分析和操纵sqlite3的数据库。 注意:SQLiteSpy是免费供个人和教育用途,SQLiteSpy主要特点: 1、数据库一览:树状显示所有的架构,包括表,列,索引和触发器在数据库中包含的项目。按F5更新架构树,双击一个表或视图来显示它的数据,使用常用的命令的上下文菜单。 2、网格单元格编辑:表格单元格中编辑,显示一个表通过树状架构,选择一个单元格,然后按F2键调用编辑器。然后修改并确认您的更改写回到表里。 3、数据类型显示:本机的SQL数据类型显示不同的背景颜色来帮助检测类型错误。类型错误可能会导致性能下降或错误的SELECT结果集,防止NULL值与空字符串混淆。 4、完全的Unicode: SQLiteSpy具有完全支持SQLite的Unicode的能力。数据显示和输入是完全实现为Unicode,包括SQL命令。 5、多个SQL编辑:现代标签是用来编辑和显示的查询语句和结果比较容易多个SQL查询。 SQL查询执行输入或加载到SQL他们编辑。然后按F9键运行该查询,或Ctrl + F9来运行当前行或选择只。 6、时间测量: SQL执行的时间会自动测量和显示,以帮助优化查询。 7、正则表达式:在SQL关键字regexp是支持,并增加了完整的Perl的正则表达式语法5.10 SQLiteSpy。的实施,实现了利用DIRegEx库。 8、数学SQL函数:下面的SQL函数可用数学除了SQLite的默认:ACOS(), ASIN(), ATAN(), ATAN(), ATAN2(), CEIL(), CEILING(), COS(), COT(), DEGREES(), EXP(), FLOOR(), LN(), LOG(), LOG(), LOG2(), LOG10(), MOD(), PI(), POW(), RADIANS(), SIGN(), SIN(), SQRT(), TAN(), TRUNCATE(). 9、数据压缩:压缩的SQL函数()适用的zlib的紧缩到任何文本或BLOB值。 10、紧凑型结果储存:使用内部数据存储机制,以达到最佳的兼容SQLite的原生数据类型。因此,SQLiteSpy使用远低于其它的SQLite管理内存和更有效地处理大量的表。 11、内建的SQLite引擎: SQLiteSpy已建成一个单一的应用程序文件与SQLite数据库引擎可执行文件。不需要部署任何DLL文件,这使得SQLiteSpy更易于部署。 12、加密支持: SQLiteSpy可以阅读和修改加密的数据库文件由DISQLite3产生。 DISQLite3实现了自己的母语AES加密。这为不符合商业SQLite的加密扩展(SSE)的或任何其他第三方的实施提供兼容。 13、易安装和卸载:要运行SQLiteSpy,只需解压SQLiteSpy.exe文件到任何目录和执行文件。不需要安装。刚开始时,该程序创建一个文件SQLiteSpy.db3(1 sqlite3的数据库)来存储的和设置。它不写任何其他文件或注册表。卸载一样只是简单的删除两个文件:应用程序的可执行文件和数据库文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值