Skip to main content

isa_lang/x86_64/
fmt.rs

1//! Intel-syntax text: the disassembler's output format.
2//!
3//! The format is fixed so that equal instructions always print the same:
4//! lowercase mnemonics and registers, `size ptr [base+index*scale+disp]`
5//! memory operands (no size for `lea`), and every number in hexadecimal with
6//! an explicit sign when negative. Branch targets print as `$+0x10` (relative
7//! to the start of the instruction, as NASM writes it) or, through
8//! [`Inst::display_at`], as an absolute address.
9
10use core::fmt;
11
12use super::inst::{Inst, Mnemonic};
13use super::operand::{Mem, Operand};
14
15/// Writes `value` as `0x..` or `-0x..`.
16pub(crate) fn hex(f: &mut fmt::Formatter<'_>, value: i64) -> fmt::Result {
17    if value < 0 {
18        write!(f, "-0x{:x}", value.unsigned_abs())
19    } else {
20        write!(f, "0x{value:x}")
21    }
22}
23
24/// Writes `value` as `+0x..` or `-0x..`.
25pub(crate) fn signed_hex(f: &mut fmt::Formatter<'_>, value: i64) -> fmt::Result {
26    if value >= 0 {
27        f.write_str("+")?;
28    }
29    hex(f, value)
30}
31
32/// Writes the bracketed address of a memory operand.
33fn address(f: &mut fmt::Formatter<'_>, m: &Mem) -> fmt::Result {
34    f.write_str("[")?;
35    let disp = i64::from(m.disp());
36    let mut any = false;
37    if m.is_rip_relative() {
38        f.write_str("rip")?;
39        any = true;
40    }
41    if let Some(base) = m.base() {
42        f.write_str(base.name())?;
43        any = true;
44    }
45    if let Some(index) = m.index() {
46        if any {
47            f.write_str("+")?;
48        }
49        write!(f, "{}*{}", index.name(), m.scale().factor())?;
50        any = true;
51    }
52    if !any {
53        hex(f, disp)?;
54    } else if disp != 0 {
55        signed_hex(f, disp)?;
56    }
57    f.write_str("]")
58}
59
60fn operand(f: &mut fmt::Formatter<'_>, op: &Operand, sized: bool, ip: Option<u64>) -> fmt::Result {
61    match op {
62        Operand::Reg(r) => f.write_str(r.name()),
63        Operand::Mem(m) => {
64            if sized {
65                write!(f, "{} ptr ", m.size().keyword())?;
66            }
67            address(f, m)
68        }
69        Operand::Imm(v) => hex(f, *v),
70        Operand::Rel(d) => match ip {
71            Some(ip) => write!(f, "0x{:x}", ip.wrapping_add(*d as u64)),
72            None => {
73                f.write_str("$")?;
74                signed_hex(f, *d)
75            }
76        },
77    }
78}
79
80fn inst(f: &mut fmt::Formatter<'_>, i: &Inst, ip: Option<u64>) -> fmt::Result {
81    write!(f, "{}", i.mnemonic())?;
82    // `lea` computes an address; the access size means nothing there.
83    let sized = i.mnemonic() != Mnemonic::Lea;
84    for (n, op) in i.operands().iter().enumerate() {
85        f.write_str(if n == 0 { " " } else { ", " })?;
86        operand(f, op, sized, ip)?;
87    }
88    Ok(())
89}
90
91impl fmt::Display for Inst {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        padded(f, self, None)
94    }
95}
96
97/// Writes an instruction, honouring a width (`{:<40}`) by rendering first.
98/// The unpadded path writes straight to the formatter, allocation-free.
99fn padded(f: &mut fmt::Formatter<'_>, i: &Inst, ip: Option<u64>) -> fmt::Result {
100    if f.width().is_none() {
101        return inst(f, i, ip);
102    }
103    f.pad(&alloc::format!("{}", Unpadded(i, ip)))
104}
105
106/// An instruction's text with no width applied.
107struct Unpadded<'a>(&'a Inst, Option<u64>);
108
109impl fmt::Display for Unpadded<'_> {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        inst(f, self.0, self.1)
112    }
113}
114
115impl fmt::Display for Operand {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        operand(f, self, true, None)
118    }
119}
120
121impl fmt::Display for Mem {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{} ptr ", self.size().keyword())?;
124        address(f, self)
125    }
126}
127
128impl fmt::Debug for Mem {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "Mem({self})")
131    }
132}
133
134/// An [`Inst`] shown with branch targets as absolute addresses, for an
135/// instruction located at a known address. Built by [`Inst::display_at`].
136///
137/// # Examples
138///
139/// ```
140/// use isa_lang::x86_64::{Inst, Mnemonic, Operand};
141///
142/// let call = Inst::new(Mnemonic::Call, [Operand::Rel(0x40)]);
143/// assert_eq!(call.display_at(0x40_1000).to_string(), "call 0x401040");
144/// assert_eq!(call.to_string(), "call $+0x40");
145/// ```
146#[derive(Clone, Copy, Debug)]
147pub struct DisplayAt<'a> {
148    inst: &'a Inst,
149    ip: u64,
150}
151
152impl Inst {
153    /// Displays the instruction as located at address `ip`, so `Rel`
154    /// targets print as absolute addresses (wrapping at 2^64).
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use isa_lang::x86_64::{Cond, Inst, Mnemonic, Operand};
160    ///
161    /// let back = Inst::new(Mnemonic::Jcc(Cond::Ne), [Operand::Rel(-16)]);
162    /// assert_eq!(back.display_at(0x1010).to_string(), "jne 0x1000");
163    /// ```
164    #[must_use]
165    pub fn display_at(&self, ip: u64) -> DisplayAt<'_> {
166        DisplayAt { inst: self, ip }
167    }
168}
169
170impl fmt::Display for DisplayAt<'_> {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        padded(f, self.inst, Some(self.ip))
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use alloc::string::ToString;
179
180    use super::*;
181    use crate::x86_64::{Reg, Scale, Size};
182
183    #[test]
184    fn test_hex_extremes() {
185        assert_eq!(Operand::Imm(i64::MIN).to_string(), "-0x8000000000000000");
186        assert_eq!(Operand::Imm(i64::MAX).to_string(), "0x7fffffffffffffff");
187        assert_eq!(Operand::Imm(0).to_string(), "0x0");
188        assert_eq!(Operand::Rel(0).to_string(), "$+0x0");
189        assert_eq!(Operand::Rel(-1).to_string(), "$-0x1");
190    }
191
192    #[test]
193    fn test_memory_text() {
194        let q = Size::Qword;
195        assert_eq!(Mem::new(q, Reg::RAX).to_string(), "qword ptr [rax]");
196        assert_eq!(Mem::rip(q, 0).to_string(), "qword ptr [rip]");
197        assert_eq!(Mem::absolute(q, -16).to_string(), "qword ptr [-0x10]");
198        assert_eq!(Mem::absolute(q, 0).to_string(), "qword ptr [0x0]");
199        let idx = Mem::absolute(q, 0).with_index(Reg::R12, Scale::X1);
200        assert_eq!(idx.to_string(), "qword ptr [r12*1]");
201        let full = Mem::new(Size::Byte, Reg::R13)
202            .with_index(Reg::RBP, Scale::X2)
203            .with_disp(i32::MIN);
204        assert_eq!(full.to_string(), "byte ptr [r13+rbp*2-0x80000000]");
205    }
206
207    #[test]
208    fn test_lea_has_no_size() {
209        let lea = Inst::new(
210            Mnemonic::Lea,
211            [
212                Reg::RAX.into(),
213                Mem::new(Size::Qword, Reg::RBX).with_disp(8).into(),
214            ],
215        );
216        assert_eq!(lea.to_string(), "lea rax, [rbx+0x8]");
217    }
218
219    #[test]
220    fn test_padding_is_honoured() {
221        let i = Inst::new(Mnemonic::Ret, []);
222        assert_eq!(alloc::format!("[{i:>5}]"), "[  ret]");
223        assert_eq!(alloc::format!("[{:<6}]", i.display_at(0)), "[ret   ]");
224    }
225
226    #[test]
227    fn test_display_at_wraps() {
228        let j = Inst::new(Mnemonic::Jmp, [Operand::Rel(-2)]);
229        assert_eq!(j.display_at(0).to_string(), "jmp 0xfffffffffffffffe");
230    }
231}