Skip to main content

sbpf_disassembler/
program.rs

1use {
2    crate::{
3        elf_header::{E_MACHINE, E_MACHINE_SBPF, ELFHeader},
4        errors::DisassemblerError,
5        program_header::ProgramHeader,
6        relocation::Relocation,
7        rodata::RodataSection,
8        section_header::SectionHeader,
9        section_header_entry::SectionHeaderEntry,
10    },
11    either::Either,
12    object::{Endianness, read::elf::ElfFile64},
13    sbpf_common::{inst_param::Number, instruction::Instruction, opcode::Opcode},
14    serde::{Deserialize, Serialize},
15    std::collections::{BTreeSet, HashMap},
16};
17
18pub type DisassembleResult =
19    Result<(Vec<Instruction>, Option<RodataSection>, Option<usize>), DisassemblerError>;
20
21#[derive(Debug, Serialize, Deserialize)]
22pub struct Program {
23    pub elf_header: ELFHeader,
24    pub program_headers: Vec<ProgramHeader>,
25    pub section_headers: Vec<SectionHeader>,
26    pub section_header_entries: Vec<SectionHeaderEntry>,
27    pub relocations: Vec<Relocation>,
28}
29
30impl Program {
31    pub fn from_bytes(b: &[u8]) -> Result<Self, DisassemblerError> {
32        let elf_file = ElfFile64::<Endianness>::parse(b).map_err(|e| {
33            eprintln!("ELF parse error: {}", e);
34            DisassemblerError::NonStandardElfHeader
35        })?;
36
37        // Parse elf header.
38        let elf_header = ELFHeader::from_elf_file(&elf_file)?;
39
40        // Parse program headers.
41        let program_headers = ProgramHeader::from_elf_file(&elf_file)?;
42
43        // Parse section headers and section header entries.
44        let (section_headers, section_header_entries) = SectionHeader::from_elf_file(&elf_file)?;
45
46        // Parse relocations.
47        let relocations = Relocation::from_elf_file(&elf_file)?;
48
49        Ok(Self {
50            elf_header,
51            program_headers,
52            section_headers,
53            section_header_entries,
54            relocations,
55        })
56    }
57
58    pub fn to_ixs(self) -> DisassembleResult {
59        self.into_ixs_inner(true)
60    }
61
62    pub fn to_ixs_raw(self) -> DisassembleResult {
63        self.into_ixs_inner(false)
64    }
65
66    fn into_ixs_inner(self, resolve_offsets: bool) -> DisassembleResult {
67        // Find and populate instructions for the .text section
68        let text_section = self
69            .section_header_entries
70            .iter()
71            .find(|e| e.label.eq(".text\0"))
72            .ok_or(DisassemblerError::MissingTextSection)?;
73        let text_section_offset = text_section.offset as u64;
74
75        // Build syscall map
76        let syscall_map = self.build_syscall_map(text_section_offset);
77
78        let data = &text_section.data;
79        if !data.len().is_multiple_of(8) {
80            return Err(DisassemblerError::InvalidDataLength);
81        }
82
83        let is_sbpf_v2 =
84            self.elf_header.e_flags == 0x02 && self.elf_header.e_machine == E_MACHINE_SBPF;
85
86        // Get rodata info
87        let rodata_info = self.get_rodata_info();
88        let (rodata_base, rodata_end) = rodata_info
89            .as_ref()
90            .map(|(d, addr)| (*addr, *addr + d.len() as u64))
91            .unwrap_or((0, 0));
92
93        // Parse instructions and build slot mappings
94        let mut ixs: Vec<Instruction> = Vec::new();
95        let mut idx_to_slot: Vec<usize> = Vec::new();
96        let mut pos: usize = 0;
97        let mut slot: usize = 0;
98
99        while pos < data.len() {
100            let remaining = &data[pos..];
101            if remaining.len() < 8 {
102                break;
103            }
104
105            // ugly v2 shit we need to fix goes here:
106            let mut ix = if is_sbpf_v2 {
107                Instruction::from_bytes_sbpf_v2(remaining)?
108            } else {
109                Instruction::from_bytes(remaining)?
110            };
111
112            // Handle syscall relocation
113            if ix.opcode == Opcode::Call
114                && let Some(Either::Right(Number::Int(-1))) = ix.imm
115                && let Some(syscall_name) = syscall_map.get(&(pos as u64))
116            {
117                ix.imm = Some(Either::Left(syscall_name.clone()));
118            }
119
120            idx_to_slot.push(slot);
121
122            if ix.opcode == Opcode::Lddw {
123                pos += 16;
124                slot += 2;
125            } else {
126                pos += 8;
127                slot += 1;
128            }
129
130            ixs.push(ix);
131        }
132
133        let mut slot_to_idx = vec![0usize; slot];
134        for (idx, &slot) in idx_to_slot.iter().enumerate() {
135            slot_to_idx[slot] = idx;
136        }
137
138        let text_sh_addr = self
139            .section_headers
140            .iter()
141            .find(|h| {
142                self.section_header_entries
143                    .iter()
144                    .any(|e| e.label.eq(".text\0") && e.offset == h.sh_offset as usize)
145            })
146            .map(|h| h.sh_addr)
147            .unwrap_or(0);
148        let text_end_addr = text_sh_addr + text_section.data.len() as u64;
149
150        let mut rodata_refs = BTreeSet::new();
151
152        if resolve_offsets {
153            // Resolve jump/call labels and collect rodata references
154
155            for (idx, ix) in ixs.iter_mut().enumerate() {
156                let is_lddw = ix.opcode == Opcode::Lddw;
157
158                // Resolve jump targets
159                if ix.is_jump()
160                    && let Some(Either::Right(off)) = &ix.off
161                {
162                    let current_slot = idx_to_slot[idx] as i64;
163                    let target_slot = current_slot + 1 + (*off as i64);
164                    if target_slot >= 0
165                        && let Some(&target_idx) = slot_to_idx.get(target_slot as usize)
166                    {
167                        let new_off = target_idx as i64 - (idx as i64 + 1);
168                        ix.off = Some(Either::Right(new_off as i16));
169                    }
170                }
171
172                // Resolve internal call targets
173                if ix.opcode == Opcode::Call
174                    && let Some(Either::Right(Number::Int(imm))) = &ix.imm
175                {
176                    let current_slot = idx_to_slot[idx] as i64;
177                    let target_slot = current_slot + 1 + *imm;
178                    if target_slot >= 0
179                        && let Some(&target_idx) = slot_to_idx.get(target_slot as usize)
180                    {
181                        let new_rel = target_idx as i64 - (idx as i64 + 1);
182                        ix.imm = Some(Either::Right(Number::Int(new_rel)));
183                    }
184                }
185
186                // Collect rodata references
187                if is_lddw && let Some(Either::Right(Number::Int(imm))) = &ix.imm {
188                    let addr = *imm as u64;
189                    if rodata_info.is_some() && addr >= rodata_base && addr < rodata_end {
190                        rodata_refs.insert(addr);
191                    } else if addr >= text_sh_addr && addr < text_end_addr {
192                        // Convert text address to instruction index for callx.
193                        let byte_offset = addr - text_sh_addr;
194                        let target_slot = (byte_offset / 8) as usize;
195                        if target_slot < slot_to_idx.len() {
196                            let ix_idx = slot_to_idx[target_slot];
197                            ix.imm = Some(Either::Right(Number::Int(ix_idx as i64)));
198                        }
199                    }
200                }
201            }
202        }
203
204        // Parse rodata section
205        let rodata = if let Some((data, base_addr)) = rodata_info {
206            let mut section = RodataSection::parse(data, base_addr, &rodata_refs);
207            let (data_relocs, text_relocs) = self.classify_relocations(
208                &section.data,
209                base_addr,
210                text_section_offset,
211                text_section.data.len() as u64,
212                text_sh_addr,
213                &slot_to_idx,
214            );
215            section.data_relocations = data_relocs;
216            section.text_relocations = text_relocs;
217            Some(section)
218        } else {
219            None
220        };
221
222        // Calculate entrypoint instruction index from byte offset.
223        let entrypoint_idx = self.get_entrypoint_offset().map(|byte_offset| {
224            let entrypoint_slot = (byte_offset / 8) as usize;
225            if entrypoint_slot < slot_to_idx.len() {
226                slot_to_idx[entrypoint_slot]
227            } else {
228                0
229            }
230        });
231
232        Ok((ixs, rodata, entrypoint_idx))
233    }
234
235    /// Build a hashmap where:
236    /// - key: relative position within .text section
237    /// - value: syscall name (sol_log_64_, sol_log_, etc.)
238    fn build_syscall_map(&self, text_section_offset: u64) -> HashMap<u64, String> {
239        self.relocations
240            .iter()
241            .filter(|r| r.is_syscall())
242            .filter_map(|r| {
243                r.symbol_name.as_ref().map(|name| {
244                    // Convert absolute offset to relative position within .text
245                    let relative_pos = r.relative_offset(text_section_offset);
246                    (relative_pos, name.clone())
247                })
248            })
249            .collect()
250    }
251
252    /// Get the raw rodata bytes and the virtual address where it's loaded in memory
253    fn get_rodata_info(&self) -> Option<(Vec<u8>, u64)> {
254        let rodata_entry = self
255            .section_header_entries
256            .iter()
257            .find(|e| e.label.starts_with(".rodata"))?;
258
259        // v3: use program header p_vaddr
260        // v0: use section header sh_addr
261        let vaddr = if self.is_v3() {
262            self.program_headers
263                .iter()
264                .find(|ph| {
265                    let rodata_offset = rodata_entry.offset as u64;
266                    rodata_offset >= ph.p_offset && rodata_offset < ph.p_offset + ph.p_filesz
267                })
268                .map(|ph| ph.p_vaddr)
269                .unwrap_or(0)
270        } else {
271            let rodata_header = self
272                .section_headers
273                .iter()
274                .find(|h| h.sh_offset as usize == rodata_entry.offset)?;
275            rodata_header.sh_addr
276        };
277
278        // Check for .data.rel.ro section and combine if present.
279        if let Some(data_rel_ro_entry) = self
280            .section_header_entries
281            .iter()
282            .find(|e| e.label.starts_with(".data.rel.ro"))
283        {
284            let data_rel_ro_header = self
285                .section_headers
286                .iter()
287                .find(|h| h.sh_offset as usize == data_rel_ro_entry.offset);
288
289            if let Some(drr_header) = data_rel_ro_header {
290                let drr_end = drr_header.sh_addr + drr_header.sh_size;
291                let total_size = (drr_end - vaddr) as usize;
292
293                // Allocate combined buffer.
294                let mut combined = vec![0u8; total_size];
295
296                // Copy .rodata at offset 0.
297                let rodata_len = rodata_entry.data.len().min(total_size);
298                combined[..rodata_len].copy_from_slice(&rodata_entry.data[..rodata_len]);
299
300                // Copy .data.rel.ro at its offset relative to rodata base.
301                let drr_offset = (drr_header.sh_addr - vaddr) as usize;
302                let drr_len = data_rel_ro_entry.data.len().min(total_size - drr_offset);
303                combined[drr_offset..drr_offset + drr_len]
304                    .copy_from_slice(&data_rel_ro_entry.data[..drr_len]);
305
306                return Some((combined, vaddr));
307            }
308        }
309
310        Some((rodata_entry.data.clone(), vaddr))
311    }
312
313    /// Classify relocations into data and text relocations.
314    fn classify_relocations(
315        &self,
316        rodata_data: &[u8],
317        rodata_base: u64,
318        text_offset: u64,
319        text_len: u64,
320        text_sh_addr: u64,
321        slot_to_idx: &[usize],
322    ) -> (Vec<usize>, Vec<(usize, usize)>) {
323        let rodata_len = rodata_data.len();
324        let text_end_addr = text_sh_addr + text_len;
325        let mut data_relocs = Vec::new();
326        let mut text_relocs = Vec::new();
327
328        for r in &self.relocations {
329            if r.rel_type != crate::relocation::RelocationType::R_BPF_64_RELATIVE {
330                continue;
331            }
332            if r.offset >= text_offset && r.offset < text_offset + text_len {
333                continue;
334            }
335            if r.offset < rodata_base || r.offset + 8 > rodata_base + rodata_len as u64 {
336                continue;
337            }
338            let offset_in_blob = (r.offset - rodata_base) as usize;
339            data_relocs.push(offset_in_blob);
340
341            let imm_offset = offset_in_blob + 4;
342            if imm_offset + 4 <= rodata_len {
343                let ptr =
344                    u32::from_le_bytes(rodata_data[imm_offset..imm_offset + 4].try_into().unwrap())
345                        as u64;
346                if ptr >= text_sh_addr && ptr < text_end_addr {
347                    let target_slot = ((ptr - text_sh_addr) / 8) as usize;
348                    if target_slot < slot_to_idx.len() {
349                        text_relocs.push((offset_in_blob, slot_to_idx[target_slot]));
350                    }
351                }
352            }
353        }
354
355        (data_relocs, text_relocs)
356    }
357
358    /// Get the entrypoint offset
359    pub fn get_entrypoint_offset(&self) -> Option<u64> {
360        let e_entry = self.elf_header.e_entry;
361
362        if self.is_v3() {
363            const V3_BYTECODE_VADDR: u64 = 1 << 32;
364            if e_entry >= V3_BYTECODE_VADDR {
365                Some(e_entry - V3_BYTECODE_VADDR)
366            } else {
367                None
368            }
369        } else {
370            let text_header = self.section_headers.iter().find(|h| {
371                self.section_header_entries
372                    .iter()
373                    .any(|e| e.label.eq(".text\0") && e.offset == h.sh_offset as usize)
374            })?;
375            let text_sh_addr = text_header.sh_addr;
376
377            if e_entry >= text_sh_addr {
378                Some(e_entry - text_sh_addr)
379            } else {
380                None
381            }
382        }
383    }
384
385    fn is_v3(&self) -> bool {
386        self.elf_header.e_flags == 0x03 && self.elf_header.e_machine == E_MACHINE
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use {
393        crate::{
394            elf_header::{E_MACHINE_SBPF, ELFHeader},
395            program::Program,
396            section_header_entry::SectionHeaderEntry,
397        },
398        hex_literal::hex,
399    };
400
401    #[test]
402    fn try_deserialize_program() {
403        let program = Program::from_bytes(&hex!("7F454C460201010000000000000000000300F700010000002001000000000000400000000000000028020000000000000000000040003800030040000600050001000000050000002001000000000000200100000000000020010000000000003000000000000000300000000000000000100000000000000100000004000000C001000000000000C001000000000000C0010000000000003C000000000000003C000000000000000010000000000000020000000600000050010000000000005001000000000000500100000000000070000000000000007000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007912A000000000007911182900000000B7000000010000002D21010000000000B70000000000000095000000000000001E0000000000000004000000000000000600000000000000C0010000000000000B0000000000000018000000000000000500000000000000F0010000000000000A000000000000000C00000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000120001002001000000000000300000000000000000656E747279706F696E7400002E74657874002E64796E737472002E64796E73796D002E64796E616D6963002E73687374727461620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010000000600000000000000200100000000000020010000000000003000000000000000000000000000000008000000000000000000000000000000170000000600000003000000000000005001000000000000500100000000000070000000000000000400000000000000080000000000000010000000000000000F0000000B0000000200000000000000C001000000000000C001000000000000300000000000000004000000010000000800000000000000180000000000000007000000030000000200000000000000F001000000000000F0010000000000000C00000000000000000000000000000001000000000000000000000000000000200000000300000000000000000000000000000000000000FC010000000000002A00000000000000000000000000000001000000000000000000000000000000")).unwrap();
404        println!("{:?}", program.section_header_entries);
405    }
406
407    #[test]
408    fn test_to_ixs_invalid_data_length() {
409        // Create program with .text section that has invalid length (not multiple of 8)
410        let program = Program {
411            elf_header: ELFHeader {
412                ei_magic: [127, 69, 76, 70],
413                ei_class: 2,
414                ei_data: 1,
415                ei_version: 1,
416                ei_osabi: 0,
417                ei_abiversion: 0,
418                ei_pad: [0; 7],
419                e_type: 0,
420                e_machine: 0,
421                e_version: 0,
422                e_entry: 0,
423                e_phoff: 0,
424                e_shoff: 0,
425                e_flags: 0,
426                e_ehsize: 0,
427                e_phentsize: 0,
428                e_phnum: 0,
429                e_shentsize: 0,
430                e_shnum: 0,
431                e_shstrndx: 0,
432            },
433            program_headers: vec![],
434            section_headers: vec![],
435            section_header_entries: vec![
436                SectionHeaderEntry::new(".text\0".to_string(), 0, vec![0x95, 0x00, 0x00]).unwrap(), // Only 3 bytes
437            ],
438            relocations: vec![],
439        };
440
441        let result = program.to_ixs();
442        assert!(result.is_err());
443        assert!(matches!(
444            result.unwrap_err(),
445            crate::errors::DisassemblerError::InvalidDataLength
446        ));
447    }
448
449    #[test]
450    fn test_to_ixs_with_lddw() {
451        // Test with 16 bytes lddw instruction
452
453        let mut lddw_bytes = vec![0x18, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
454        lddw_bytes.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
455        lddw_bytes.extend_from_slice(&[0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // exit
456
457        let program = Program {
458            elf_header: ELFHeader {
459                ei_magic: [127, 69, 76, 70],
460                ei_class: 2,
461                ei_data: 1,
462                ei_version: 1,
463                ei_osabi: 0,
464                ei_abiversion: 0,
465                ei_pad: [0; 7],
466                e_type: 0,
467                e_machine: E_MACHINE_SBPF,
468                e_version: 0,
469                e_entry: 0,
470                e_phoff: 0,
471                e_shoff: 0,
472                e_flags: 0,
473                e_ehsize: 0,
474                e_phentsize: 0,
475                e_phnum: 0,
476                e_shentsize: 0,
477                e_shnum: 0,
478                e_shstrndx: 0,
479            },
480            program_headers: vec![],
481            section_headers: vec![],
482            section_header_entries: vec![
483                SectionHeaderEntry::new(".text\0".to_string(), 0, lddw_bytes).unwrap(),
484            ],
485            relocations: vec![],
486        };
487
488        let (ixs, _, _) = program.to_ixs().unwrap();
489        assert_eq!(ixs.len(), 2); // lddw + exit
490        assert_eq!(ixs[0].opcode, sbpf_common::opcode::Opcode::Lddw);
491    }
492
493    #[test]
494    fn test_to_ixs_sbpf_v2() {
495        // Use a v2 opcode (0x8C -> ldxw in v2)
496        let v2_bytes = vec![0x8c, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
497
498        let program = Program {
499            elf_header: ELFHeader {
500                ei_magic: [127, 69, 76, 70],
501                ei_class: 2,
502                ei_data: 1,
503                ei_version: 1,
504                ei_osabi: 0,
505                ei_abiversion: 0,
506                ei_pad: [0; 7],
507                e_type: 0,
508                e_machine: E_MACHINE_SBPF,
509                e_version: 0,
510                e_entry: 0,
511                e_phoff: 0,
512                e_shoff: 0,
513                e_flags: 0x02, // SBPF v2 flag
514                e_ehsize: 0,
515                e_phentsize: 0,
516                e_phnum: 0,
517                e_shentsize: 0,
518                e_shnum: 0,
519                e_shstrndx: 0,
520            },
521            program_headers: vec![],
522            section_headers: vec![],
523            section_header_entries: vec![
524                SectionHeaderEntry::new(".text\0".to_string(), 0, v2_bytes).unwrap(),
525            ],
526            relocations: vec![],
527        };
528
529        let (ixs, _, _) = program.to_ixs().unwrap();
530        assert_eq!(ixs.len(), 1);
531        assert_eq!(ixs[0].opcode, sbpf_common::opcode::Opcode::Ldxw);
532    }
533}