从字节码层面看“HelloWorld”
HelloWorld 字节码生成
众所周知,Java 程序是在 JVM 上运行的,不过 JVM 运行的其实不是 Java 语言本身,而是 Java 程序编译成的字节码文件。可能一开始 JVM 是为 Java 语言服务的,不过随着编译技术和 JVM 自身的不断发展和成熟,JVM 已经不仅仅只运行 Java 程序。任何能编译成为符合 JVM 字节码规范的语言都可以在 JVM 上运行,比较常见的 Scala、Groove、JRuby等。今天,我就从大家最熟悉的程序“HelloWorld”程序入手,分析整个 Class 文件的结构。虽然这个程序比较简单,但是基本上包含了字节码规范中的所有内容,因此即使以后要分析更复杂的程序,那也只是“量”上的变化,本质上没有区别。
我的汇编学习之路(8)
这是系列的第八篇(译者注:最终篇),接下来讲讲怎么在汇编使用非整型(浮点数)。
我们有一些方法可以处理浮点数据:
- fpu
- sse
首先让我们看看如何将浮点数存储在内存中。有三种浮点数据类型:
- 单精度
- 双精度
- 双倍扩展精度
正如英特尔的64-ia-32架构软件开发者-vol-1手册所述:
The data formats for these data types correspond directly to formats specified in the IEEE Standard 754 for Binary Floating-Point Arithmetic.
内存中呈现的单精度浮点浮点数据:
- 符号 - 1位
- 指数 - 8位
- 尾数 - 23位
我的汇编学习之路(7)
这是系列的第七篇,接下来要讲一下如何在c中使用汇编。
其实我们有三种方法:
- 从C代码调用汇编例程
- 从汇编代码调用c例程
- 在C代码中使用内联汇编
我们来编写3个简单的Hello World程序,向我们展示如何使用assembly和C在一起。
我的汇编学习之路(6)
这是系列的第六篇,接下来要讲一下AT&T汇编语法。之前一直都是用nasm汇编,但是还有其他不同语法的汇编,如fasm,yasm等等。正如我上面写的,我们要看一下gas(GNU汇编)和讲一下与nasm的不同。GCC用的是GNU汇编,所以你会看到一个简单的hello world的汇编输出:1
2
3
4
5
6#include <unistd.h>
int main(void) {
write(1, "Hello World\n", 15);
return 0;
}
汇编输出: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 .file "test.c"
.section .rodata
.LC0:
.string "Hello World\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $15, %edx
movl $.LC0, %esi
movl $1, %edi
call write
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
.section .note.GNU-stack,"",@progbits
看上去跟nasm汇编不同吧,让我们看看他们的不同吧。
我的汇编学习之路(5)
我的汇编学习之路(4)
前段时间,我开始为x86_64编写有关汇编编程的一系列博文。你可以通过asm标签找到它。不幸的是,我上次很忙,没有新帖,所以今天我继续写关于汇编的帖子,并且每周都会尝试这样做。
今天我们将看一下字符串和一些字符串操作。我们还是使用nasm汇编器和linux x86_64。
反转字符串
当然,当我们谈论汇编编程语言时,我们不能谈论字符串数据类型,实际上我们正在处理字节数组。我们尝试写简单的例子,我们将定义字符串数据,并尝试反转并将结果写入stdout。当我们开始学习新的编程语言时,这个任务似乎很简单和受欢迎。我们来看看实现。
首先,我定义初始化的数据。它将被放置在数据部分(您可以部分阅读有关章节):1
2
3
4
5
6
7
8section .data
SYS_WRITE equ 1
STD_OUT equ 1
SYS_EXIT equ 60
EXIT_CODE equ 0
NEW_LINE db 0xa
INPUT db "Hello world!"
我的汇编学习之路(3)
栈是在内存中是一个特殊的区域,它的主要操作是lifo(后进先出)
我们有16个通用的寄存器,用来存储临时数据。它们分别是RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP 和 R8-R15。对于真实的应用程序来说,这16个寄存器太少了,所以我们用栈存储数据。另外,栈还有其他用法:当你调用一个函数,函数的返回地址拷贝到栈。当函数执行完后,地址从栈拷贝到命令计数器(RIP),应用程序就可以从函数调用的下个位置继续执行。
举个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16global _start
section .text
_start:
mov rax, 1
call incRax
cmp rax, 2
jne exit
;;
;; Do something
;;
incRax:
inc rax
ret
我的汇编学习之路(2)
对于不折不扣的汇编新手来说,第一部分中出现的很多概念可能不是很明白,于是我决定写更多有价值的文章。所以,让我们开始《我的汇编学习之路》的第二部分的学习。
术语和概念
当我写了第一篇之后,我从不同的读者那获得很多反馈,第一篇中有些部分不明白,这就是本文以及接下来几篇从一些术语的描述开始的原因。
寄存器(Register):寄存器是处理器内小容量的存储结构,处理器的主要功能是数据处理,处理器可以从内存中获得数据,但这是一种低速的操作,这就是为什么处理器为什么要有自己数据存储结构,称为“寄存器”。
L小端(Little-endian):我们可以假设内存是一个大的数组,它包含一个字节一个字节的数。每个地址存储了内存“数组”中的一个元素,每个元素一个字节。举例来说,我们有 4 字节数:AA 56 AB FF,在小端模式下最低位存放在低地址上:1
2
3
40 FF
1 AB
2 56
3 AA
这里,0、1、2、3 是内存地址。
大端(Big-endian):大端存储数据与小端相反。所有上面的字节序在大端模式下是:1
2
3
40 AA
1 56
2 AB
3 FF
系统调用(Syscall):系统调用是用户程序要求操作系统为其完成某些工作的一种方式。你可以在这里找到系统调用表。
栈(Stack):处理器中寄存器的个数非常有限。所以栈是一块连续的内存空间,可以通过特殊寄存器如 RSP、SS、RIP 等来寻址。在接下来的文章我会专门深入介绍栈。
我的汇编学习之路(1)
引言
我们很多人是开发者,每天写大量的代码,有时也不是糟糕的代码。每个人都能很轻松写下这样的代码:1
2
3
4
5
6
7
8#include <stdio.h>
int main() {
int x = 10;
int y = 100;
printf("x + y = %d", x + y);
return 0;
}
大家都能理解上面这段 C 语言代码完成的功能,但是…这段代码底层是如何工作的呢?我想我们中间不是所有人都能回答这个问题,我也不能。我认为我可以用高级编程语言写代码,例如 Haskell、Erlang、Go 等等,但是我完全不知道在编译之后它在底层是如何工作的。所以,我决定往下再深入一步,到汇编这个层次,并且记录下我的学习汇编之路。希望这是有趣的过程,而不是仅仅对我一个人。大约五、六年前我已经使用过汇编来写简单的程序,那时我还在上大学,用的是 Turbo 汇编和 DOS 操作系统。现在我使用 Linux-x86_64 操作系统,是的,64 位 Linux 和 16 位 DOS 肯定有很大的不同。那我们就开始吧。