direct_asm/
lib.rs

1use std::process;
2use std::io::Write;
3
4extern crate proc_macro;
5
6mod att;
7mod x86;
8
9use proc_macro::{Delimiter, Literal, Group, Punct, Spacing, TokenStream, TokenTree};
10use quote::{quote, ToTokens};
11use rand::{thread_rng, Rng};
12
13#[proc_macro_attribute]
14pub fn assemble(args: TokenStream, input: TokenStream) -> TokenStream {
15    let attr = syn::parse_macro_input!(args as syn::AttributeArgs);
16    let mut assembler: Box<dyn Assembler> = choose_backed(&attr);
17
18    let (head, body) = split_function(input);
19    let asm_input = get_body(body);
20
21
22    let raw = assembler.assemble(&asm_input);
23    let len = raw.len();
24    let definition = {
25        let mut items = TokenStream::new();
26        for byte in &raw {
27            if !items.is_empty() {
28                items.extend(Some(TokenTree::Punct(Punct::new(',', Spacing::Alone))));
29            }
30            items.extend(Some(TokenTree::Literal(Literal::u8_unsuffixed(*byte))));
31        }
32        let tree = TokenTree::Group(Group::new(Delimiter::Bracket, items));
33        let stream = TokenStream::from(tree);
34        proc_macro2::TokenStream::from(stream)
35    };
36
37    let unique_name = choose_link_name();
38    let unique_ident = syn::Ident::new(&unique_name, proc_macro2::Span::call_site());
39    let mut binary_symbol = quote! {
40        mod #unique_ident {
41            #[link_section=".text"]
42            #[no_mangle]
43            static #unique_ident: [u8; #len] = #definition;
44        }
45    };
46
47    let function_def = syn::ForeignItem::Fn(syn::ForeignItemFn {
48        attrs: vec![syn::parse_quote!(#[link_name=#unique_name])],
49        vis: head.visibility,
50        sig: head.function_def,
51        semi_token: syn::token::Semi::default(),
52    });
53
54    let function_symbol = quote! {
55        extern "C" {
56            #function_def
57        }
58    };
59
60    binary_symbol.extend(function_symbol);
61    binary_symbol.into()
62}
63
64fn choose_backed(attr: &[syn::NestedMeta]) -> Box<dyn Assembler> {
65    enum Backend {
66        GnuAs,
67        Nasm,
68        Dynasm,
69    }
70
71    let backend = match &attr {
72        [syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue { path , lit, .. }))] => {
73            if path.is_ident("backend") {
74                if let syn::Lit::Str(st) = lit {
75                    match st.value().as_str() {
76                        "nasm" => Backend::Nasm,
77                        "dynasm" => Backend::Dynasm,
78                        "gnu-as" | "gnuas" | "gas" | "as" => Backend::GnuAs,
79                        _ => panic!("Unknown backend (nasm, dynasm, gnuas, gnu-as, gas, as)"),
80                    }
81                } else {
82                    panic!("Expected string value identifying backend");
83                }
84            } else {
85                panic!("Unexpected keyword")
86            }
87        },
88        [] => Backend::Dynasm,
89        _ => panic!("Backend is unknown"),
90    };
91
92    match backend {
93        Backend::GnuAs => Box::new(GnuAs {}),
94        Backend::Nasm => Box::new(Nasm),
95        Backend::Dynasm => Box::new(x86::DynasmX86::new()),
96    }
97}
98
99/// Split the function head and body.
100fn split_function(input: TokenStream) -> (Head, TokenStream) {
101    let mut fn_item = syn::parse::<syn::ItemFn>(input)
102        .expect("Must annotate a method definition");
103    // It should be declared as such because we put it into an `extern "C"` block ..
104    assert!(fn_item.sig.abi.is_some(), "Must specify function as having C abi");
105    // .. but remove it since the actual definition we output can not have it.
106    fn_item.sig.abi = None;
107    fn_item.sig.unsafety = None;
108    let head = Head {
109        function_def: fn_item.sig,
110        visibility: fn_item.vis,
111    };
112    (head, fn_item.block.to_token_stream().into())
113}
114
115fn get_body(block: TokenStream) -> String {
116    let body;
117    match &block.into_iter().next() {
118        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => {
119            body = group.stream();
120        },
121        _ => panic!("Expected function body"),
122    };
123
124    let parts = body.into_iter().map(|item| match &item {
125        TokenTree::Literal(literal) => {
126            let stream = TokenTree::Literal(literal.clone()).into();
127            let litstr = syn::parse::<syn::LitStr>(stream)
128                .expect("Body only contain string literals");
129            litstr.value()
130        },
131        TokenTree::Punct(punc) if punc.as_char() == ';' => "\n".to_string(),
132        other => panic!("Unexpected body content: {:?}", other),
133    });
134
135    parts.collect()
136}
137
138/// Generate a random (196-bit) unique identifier for the symbol link in the proc macro.
139///
140/// To execute the trick of re-interpreting a byte stream as a function we must choose a common
141/// link name between the symbol and the later function definition that imports that symbol. This
142/// should not collide with other defined symbols, as that might silently be unsafe.
143fn choose_link_name() -> String {
144    const CHOICES: &[u8; 64] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ZZ";
145    let mut randoms = [0u8; 32];
146    thread_rng().fill(&mut randoms);
147
148    let random = randoms
149        .iter()
150        .map(|idx| usize::from(idx & 63))
151        .map(|idx| std::char::from_u32(CHOICES[idx].into()).unwrap())
152        .collect::<String>();
153
154    format!("_direct_asm_{}", random)
155}
156
157struct Head {
158    function_def: syn::Signature,
159    visibility: syn::Visibility,
160}
161
162trait Assembler {
163    fn assemble(&mut self, input: &str) -> Vec<u8>;
164}
165
166struct Nasm;
167
168struct GnuAs {
169}
170
171fn nasmify(input: &str) -> Vec<u8> {
172    let input = format!("[BITS 64]\n{}", input);
173    std::fs::write("target/indirection.in", &input).unwrap();
174
175    let mut nasm = process::Command::new("nasm")
176        .stdin(process::Stdio::piped())
177        .stdout(process::Stdio::piped())
178        .stderr(process::Stdio::piped())
179        .args(&["-f", "bin", "-o", "/proc/self/fd/1", "target/indirection.in"])
180        .spawn()
181        .expect("Failed to spawn assembler");
182
183    let stdin = nasm.stdin.as_mut().expect("Nasm must accept piped input");
184    stdin.write_all(input.as_bytes()).expect("Failed to supply nasm with input");
185    stdin.flush().expect("Failed to flush");
186
187    let output = nasm.wait_with_output().expect("Failed to wait for nasm");
188    if !output.status.success() || !output.stderr.is_empty() {
189        panic!("Nasm failed: {}", String::from_utf8_lossy(&output.stderr));
190    }
191
192    output.stdout
193}
194
195impl Assembler for Nasm {
196    fn assemble(&mut self, input: &str) -> Vec<u8> {
197        nasmify(input)
198    }
199}
200
201impl Assembler for GnuAs {
202    fn assemble(&mut self, original_input: &str) -> Vec<u8> {
203        let newlined;
204        let input: &str;
205
206        if original_input.chars().rev().next() != Some('\n') {
207            newlined = format!("{}\n", original_input);
208            input = &newlined;
209        } else {
210            input = original_input;
211        }
212
213        const ASSEMBLED_FILE: &str = "target/gnu-as.out";
214        // Some arguments for reference:
215        // target selection: -march=<name>
216        // --32, --64, --x32 for isa qualification
217        // -n do not optimize alignment
218        // -mmnemonic/-msyntax=[att|intel]
219        let mut as_ = process::Command::new("as")
220            // We act as if this was safe, the least we can do is check thoroughly.
221            .arg("-msse-check=error")
222            .arg("-moperand-check=error")
223            .arg("-mmnemonic=intel")
224            .arg("-msyntax=intel")
225            .args(&["-o", ASSEMBLED_FILE])
226            .stdin(process::Stdio::piped())
227            .stdout(process::Stdio::piped())
228            .stderr(process::Stdio::piped())
229            .spawn()
230            .expect("Failed to spawn assembler");
231
232        let stdin = as_.stdin.as_mut().expect("As must accept piped input");
233        stdin.write_all(input.as_bytes()).expect("Failed to supply as with input");
234        stdin.flush().expect("Failed to flush");
235
236        let output = as_.wait_with_output().expect("Failed to wait for as");
237        if !output.status.success() || !output.stderr.is_empty() {
238            panic!("Gnu As failed: {}", String::from_utf8_lossy(&output.stderr));
239        }
240
241        // gnu as will always output ELF. We only need the binary from it. Better hope you didn't
242        // use any tables or so, as those will be dropped in the process.
243        // TODO: fail loudly.
244        let status = process::Command::new("objcopy")
245            .args(&["-O", "binary"])
246            .arg(ASSEMBLED_FILE)
247            .status()
248            .expect("Failed to spawn `objcopy`");
249        assert!(status.success(), "`objcopy` failed");
250
251        std::fs::read(ASSEMBLED_FILE).expect("No output produced")
252    }
253}