A gentle intro to assembly with Rust

栏目: IT技术 · 发布时间: 4年前

内容简介:One of the things I’ve wanted to do for a while is really dig into assembly and get into the weeds of how programs actually run. A rework of theAnd compared to some other ways I’ve tried to approach this there’s a lot less setup we need to do if we just us

One of the things I’ve wanted to do for a while is really dig into assembly and get into the weeds of how programs actually run. A rework of the asm macro has recently landed in nightly rust so it seemed like a good time.

And compared to some other ways I’ve tried to approach this there’s a lot less setup we need to do if we just use the rust playground to do all the heavy lifting.

My process for figuring things out has been pretty simple. I write a tiny bit of rust code, look at the assembly output and try to figure out what’s going on (with lots of googling). I’m going to walk you through what I did, and what I figured out.

Let’s start with the simplest possible thing I can think of:

fn main() {
    1 + 2;
}

playground link

You can get the assembly output for this by clicking the three dots next to run and selecting asm from the dropdown. You will probably also want to change the flavour (often referred to as syntax elsewhere) of assembly to intel (rather than at&t)if it isn’t already, by clicking the toggle under the config menu.

The assembly output from this in debug mode is far more massive than you’d expect - I get 157 lines. And most of it isn’t our program. The code we’ve written should be fairly easy to find though, as the compiler helpfully labels all of the functions with their crate and function names. In this case since we’re in the playground, the create is implicitly playground , so we can find our code by searching with ctrl-f for playground::main . Doing this gets me to:

playground::main: # @playground::main
# %bb.0:
    ret
                                        # -- End function

So even though this is a debug build, evidently there’s still some optimization going on, since there’s no numbers or anything that looks like it’s adding them together. All that’s happening here is we’re returning ( ret ) back to the function that called playground::main . Everything prefixed with # is a comment, and therefore ignored when we run this code.

The only other point of interest is the label playground::main: - anything suffixed with : is a label we can jump to with various commands, and indeed if we continue searching for playground::main we can find a rather indirected call to it in main . Hopefully by the end of this we’ll be understand that!

Avoiding optimizations

For now, let’s try and evade whatever’s doing the optimization:

fn add() -> usize {
    1 + 2
}

fn main() {
    add();
}

playground link

Again, searching for playground::main get us to:

playground::add: # @playground::add
# %bb.0:
    mov eax, 3
    ret
                                        # -- End function

playground::main: # @playground::main
# %bb.0:
    push    rax
    call    playground::add
# %bb.1:
    pop rax
    ret
                                        # -- End function

So we’ve got a bit more progress here. Still some optimization going on, since we don’t see 1 or 2 in the code, just 3. We can see that being moved ( mov ) into the eax register in playground::add . This must be how we’re returning the value back up to main .

And indeed, inside main we can see push rax - saving the value in the register rax to the stack, then a call to our add function, then we pop rax off the stack. The push call pop sequence is to preserve whatever values are in the registers used in add . It also just throws away the value we saved in eax in add , because eax and rax are the same register. The table here shows how ‘skinnier’ registers overlap with their ‘wider’ counterparts.

Avoiding optimizations, take 2

So how can we make this actually do some math? Let’s try again:

fn add(i: usize) -> usize {
    1 + i
}

fn main() {
    add(2);
}

playground link

So we’ve got a lot more going on this time:

playground::add: # @playground::add
# %bb.0:
    sub rsp, 24
    mov qword ptr [rsp + 16], rdi
    add rdi, 1
    setb al
    test al, 1
    mov qword ptr [rsp + 8], rdi # 8-byte Spill
    jne .LBB8_2
# %bb.1:
    mov rax, qword ptr [rsp + 8] # 8-byte Reload
    add rsp, 24
    ret

.LBB8_2:
    lea rdi, [rip + str.0]
    lea rdx, [rip + .L__unnamed_2]
    mov rax, qword ptr [rip + core::panicking::panic@GOTPCREL]
    mov esi, 28
    call rax
    ud2
                                        # -- End function

playground::main: # @playground::main
# %bb.0:
    push rax
    mov edi, 2
    call playground::add
# %bb.1:
    pop rax
    ret
                                        # -- End function

The thing we were actually trying to produce is finally in there! We can see add rdi, 1 in the output, surrounded by a pile of other stuff. So what is all this other code?

Let’s start from the top of the call stack in main . First we can see 2 is stored in the edi register before we call playground::add , so we know our argument must be in the edi register. Again, we can see the push , pop on rax , so that must be the return value.

Looking inside the function

Now, looking into playground::add we first see sub rsp, 24 . rsp is the register that holds the stack pointer, so this is growing the stack (since the stack grows downwards in x86). Further down we can see we shrink the stack by the corresponding amount with add rsp, 24 .

Then we have mov qword ptr [rsp + 16], rdi . This is copying the value from rdi onto the stack at rsp + 16 - the top of the region we just grew the stack by. The qword ptr (quadword (i.e. 64bit) pointer) bit is a hint to disambiguate the argument. Why is that pushed that onto the stack? I think this is just to make it easier to debug, since we don’t ever access that value again.

In any case, we then proceed on to actually adding 1 to rdi . The value is stored back in rdi , and importantly for what comes next, we may set some of the flags .

Then it gets complicated again - we’ve got setb al . All of the set* instructions deal with the flag register. The flag register is possibly the most magical of registers, since it’s manipulated by a bunch of instructions as a side effect.

The last instruction we ran was add , which sets 6 of the the flags: carry , parity , adjust (aka auxiliary carry) , zero , sign and overflow

In this case we’re checking if the carry bit is set, and then setting the al register to 1 if that’s the case. What is this actually doing though? The carry bit gets set to 1 if there is a carry from the two numbers we add, meaning the resulting number is too big to be stored in the register. What should we do in that case? Let’s read on to find out.

Then in the next line ( test al, 1 ) we’re checking if the value in al is equal to one. ( test does a a bitwise and operation on the two arguments - like & in rust.) This sets some more flags, notably the zero flag, which is then read by the following jne instruction.

jne stands for jump if not equal (and again there’s a series of other j* instructions). Since it uses flags, it just takes a single argument: where to jump to.

Looking at where that jumps to gives us a big hint about the intent of the logic above: core::panicking::panic@GOTPCREL really gives it away. Basically all of this chunk of assembly from setb to jne is checking if we’ve overflowed the register and panicking if we have.

The one bit we didn’t discuss is mov qword ptr [rsp + 8], rdi # 8-byte Spill . As the comment implies this is “spilling” the value from the rdi register onto the stack, since the code we’re possibly about to jump to might overwrite that register - immediately after the jne we load the value back off the stack.

Finally we shuffle the stack pointer back to it’s starting point, and ret back to the caller. ret uses the last value on the stack (which is pushed by call ) to figure out where to jump back to, so moving the stack pointer back is very important.

So maybe at this point we’ve seen enough to take a stab at replacing the guts of the add function with the asm! macro. Since we’re interested in performance, we’ll ignore those pesky overflow checks, and just assume that we’re within the bounds of u64 .

The biggest new thing we’ll have to deal with here is specifying the in and out registers. The rfc has a very approachable explaination of these, so I’d recommend reading that. There’s a skeleton you can start with here , if you want to have a go yourself.

The version I’ve cooked up looks like this . This is probably the “fanciest” possible version of this, since we’re using as many features of the asm macro as possible:

  • we’re letting the rust compiler pick the register we use, and then writing it in using the format string behaviour of the asm macro.
  • we’re also using inlateout to hint that we can just use a single register.

This seems like a reasonable point at which to break. We’ve covered a reasonable chunk of the instruction set in x64 assembly, and seen examples of most of the classes of instructions. There’s tons more we can explore, like:

  • How do loops work?
  • What happens when we use values that don’t just fit in registers?
  • How do we make a syscall?

Hopefully the resources I’ve linked to from here are sufficent for you to continue digging in if you want, and maybe I’ll manage to follow this up.


以上所述就是小编给大家介绍的《A gentle intro to assembly with Rust》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Kotlin实战

Kotlin实战

【美】Dmitry Jemerov(德米特里·詹莫瑞福)、【美】 Svetlana Isakova(斯维特拉娜·伊凡诺沃) / 覃宇、罗丽、李思阳、蒋扬海 / 电子工业出版社 / 2017-8 / 89.00

《Kotlin 实战》将从语言的基本特性开始,逐渐覆盖其更多的高级特性,尤其注重讲解如何将 Koltin 集成到已有 Java 工程实践及其背后的原理。本书分为两个部分。第一部分讲解如何开始使用 Kotlin 现有的库和API,包括基本语法、扩展函数和扩展属性、数据类和伴生对象、lambda 表达式,以及数据类型系统(着重讲解了可空性和集合的概念)。第二部分教你如何使用 Kotlin 构建自己的 ......一起来看看 《Kotlin实战》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具