Skip to main content

sbpf_assembler/
program.rs

1use {
2    crate::{
3        debug::{self, DebugData, reuse_debug_sections},
4        dynsym::{DynamicSymbol, RelDyn, RelocationType},
5        header::{ElfHeader, ProgramHeader},
6        parser::ProgramLayout,
7        section::{
8            DebugSection, DynStrSection, DynSymSection, DynamicSection, NullSection, RelDynSection,
9            Section, SectionType, ShStrTabSection,
10        },
11    },
12    std::{fs::File, io::Write, path::Path},
13};
14
15#[derive(Debug)]
16pub struct Program {
17    pub elf_header: ElfHeader,
18    pub program_headers: Option<Vec<ProgramHeader>>,
19    pub sections: Vec<SectionType>,
20}
21
22impl Program {
23    pub fn from_parse_result(
24        ProgramLayout {
25            code_section,
26            data_section,
27            dynamic_symbols,
28            relocation_data,
29            prog_is_static,
30            arch,
31            debug_sections,
32        }: ProgramLayout,
33        debug_data: Option<DebugData>,
34    ) -> Self {
35        let mut elf_header = ElfHeader::new();
36        let mut program_headers = None;
37
38        let bytecode_size = code_section.size();
39        let rodata_size = data_section.size();
40
41        let has_rodata = rodata_size > 0;
42        let ph_count = if arch.is_v3() {
43            if has_rodata { 2 } else { 1 }
44        } else if prog_is_static {
45            0
46        } else {
47            3
48        };
49
50        elf_header.e_flags = arch.e_flags();
51        elf_header.e_phnum = ph_count;
52
53        // save read + execute size for program header before
54        // ownership of code/data sections is transferred
55        let text_size = bytecode_size + rodata_size;
56
57        // Calculate base offset after ELF header and program headers
58        let base_offset = 64 + (ph_count as u64 * 56); // 64 bytes ELF header, 56 bytes per program header
59        let mut current_offset = base_offset;
60
61        let text_offset = if arch.is_v3() && has_rodata {
62            rodata_size + base_offset
63        } else {
64            base_offset
65        };
66
67        // Get the entry point offset from dynamic_symbols if available
68        let entry_point_offset = dynamic_symbols
69            .get_entry_points()
70            .first()
71            .map(|(_, offset)| *offset)
72            .unwrap_or(0);
73
74        elf_header.e_entry = if arch.is_v3() {
75            ProgramHeader::V3_BYTECODE_VADDR + entry_point_offset
76        } else {
77            text_offset + entry_point_offset
78        };
79
80        // Create a vector of sections
81        let mut sections = Vec::new();
82        sections.push(SectionType::Default(NullSection::new()));
83
84        let mut section_names = Vec::new();
85        let has_debug_sections = debug_data.is_some() || !debug_sections.is_empty();
86
87        // Add section_names in fixed order for shstrtab
88        section_names.push(".text".to_string());
89        if has_rodata {
90            section_names.push(".rodata".to_string());
91        }
92
93        if arch.is_v3() && has_rodata {
94            // Data section
95            let mut rodata_section = SectionType::Data(data_section);
96            rodata_section.set_offset(current_offset);
97            rodata_section.set_vaddr(ProgramHeader::V3_RODATA_VADDR);
98            current_offset += rodata_section.size();
99            sections.push(rodata_section);
100
101            // Code section
102            let mut text_section = SectionType::Code(code_section);
103            text_section.set_offset(current_offset);
104            text_section.set_vaddr(ProgramHeader::V3_BYTECODE_VADDR);
105            current_offset += text_section.size();
106            sections.push(text_section);
107        } else {
108            // Code section
109            let mut text_section = SectionType::Code(code_section);
110            text_section.set_offset(current_offset);
111            if arch.is_v3() {
112                text_section.set_vaddr(ProgramHeader::V3_BYTECODE_VADDR);
113            }
114            current_offset += text_section.size();
115            sections.push(text_section);
116
117            // Data section (if any)
118            if has_rodata {
119                let mut rodata_section = SectionType::Data(data_section);
120                rodata_section.set_offset(current_offset);
121                if arch.is_v3() {
122                    rodata_section.set_vaddr(ProgramHeader::V3_RODATA_VADDR);
123                }
124                current_offset += rodata_section.size();
125                sections.push(rodata_section);
126            }
127        }
128
129        let padding = (8 - (current_offset % 8)) % 8;
130        current_offset += padding;
131
132        if arch.is_v3() {
133            // v3 programs are loaded entirely through program headers; the
134            // loader never reads the section header table. Unless the program
135            // is built in debug mode, we omit section headers along with the
136            // .shstrtab and debug sections that exist only to support them,
137            // keeping v3 binaries minimal.
138            if has_rodata {
139                // 2 headers: rodata (PF_R) then bytecode (PF_X)
140                let rodata_offset = base_offset;
141                let bytecode_offset = base_offset + rodata_size;
142                program_headers = Some(vec![
143                    ProgramHeader::new_load(rodata_offset, rodata_size, false, arch),
144                    ProgramHeader::new_load(bytecode_offset, bytecode_size, true, arch),
145                ]);
146            } else {
147                // 1 header: bytecode only (PF_X)
148                program_headers = Some(vec![ProgramHeader::new_load(
149                    base_offset,
150                    bytecode_size,
151                    true,
152                    arch,
153                )]);
154            }
155
156            if has_debug_sections {
157                // If debug info is present, generate debug sections
158                let debug_sections = Self::generate_debug_sections(
159                    debug_sections,
160                    &debug_data,
161                    ProgramHeader::V3_BYTECODE_VADDR,
162                    &mut section_names,
163                    &mut current_offset,
164                );
165
166                for debug_section in debug_sections {
167                    sections.push(debug_section);
168                }
169
170                let mut shstrtab_section = SectionType::ShStrTab(ShStrTabSection::new(
171                    (section_names
172                        .iter()
173                        .map(|name| name.len() + 1)
174                        .sum::<usize>()
175                        + 1) as u32,
176                    section_names,
177                ));
178                shstrtab_section.set_offset(current_offset);
179                current_offset += shstrtab_section.size();
180                sections.push(shstrtab_section);
181            }
182        } else if !prog_is_static {
183            let mut symbol_names = Vec::new();
184            let mut dyn_syms = Vec::new();
185            let mut dyn_str_offset = 1;
186
187            dyn_syms.push(DynamicSymbol::new(0, 0, 0, 0, 0, 0));
188
189            // all symbols handled right now are all global symbols
190            for (name, _) in dynamic_symbols.get_entry_points() {
191                symbol_names.push(name.clone());
192                dyn_syms.push(DynamicSymbol::new(
193                    dyn_str_offset as u32,
194                    0x10,
195                    0,
196                    1,
197                    elf_header.e_entry,
198                    0,
199                ));
200                dyn_str_offset += name.len() + 1;
201            }
202
203            for (name, _) in dynamic_symbols.get_call_targets() {
204                symbol_names.push(name.clone());
205                dyn_syms.push(DynamicSymbol::new(dyn_str_offset as u32, 0x10, 0, 0, 0, 0));
206                dyn_str_offset += name.len() + 1;
207            }
208
209            let mut rel_count = 0;
210            let mut rel_dyns = Vec::new();
211            for (offset, rel_type, name) in relocation_data.get_rel_dyns() {
212                if rel_type == RelocationType::RSbfSyscall {
213                    if let Some(index) = symbol_names.iter().position(|n| *n == name) {
214                        rel_dyns.push(RelDyn::new(
215                            offset + text_offset,
216                            rel_type as u64,
217                            index as u64 + 1,
218                        ));
219                    } else {
220                        panic!("Symbol {} not found in symbol_names", name);
221                    }
222                } else if rel_type == RelocationType::RSbf64Relative {
223                    rel_count += 1;
224                    rel_dyns.push(RelDyn::new(offset + text_offset, rel_type as u64, 0));
225                }
226            }
227            // create four dynamic related sections
228            let mut dynamic_section = SectionType::Dynamic(DynamicSection::new(
229                (section_names
230                    .iter()
231                    .map(|name| name.len() + 1)
232                    .sum::<usize>()
233                    + 1) as u32,
234            ));
235            section_names.push(dynamic_section.name().to_string());
236
237            let mut dynsym_section = SectionType::DynSym(DynSymSection::new(
238                (section_names
239                    .iter()
240                    .map(|name| name.len() + 1)
241                    .sum::<usize>()
242                    + 1) as u32,
243                dyn_syms,
244            ));
245            section_names.push(dynsym_section.name().to_string());
246
247            let mut dynstr_section = SectionType::DynStr(DynStrSection::new(
248                (section_names
249                    .iter()
250                    .map(|name| name.len() + 1)
251                    .sum::<usize>()
252                    + 1) as u32,
253                symbol_names,
254            ));
255            section_names.push(dynstr_section.name().to_string());
256
257            let mut rel_dyn_section = SectionType::RelDyn(RelDynSection::new(
258                (section_names
259                    .iter()
260                    .map(|name| name.len() + 1)
261                    .sum::<usize>()
262                    + 1) as u32,
263                rel_dyns,
264            ));
265            section_names.push(rel_dyn_section.name().to_string());
266
267            dynamic_section.set_offset(current_offset);
268            if let SectionType::Dynamic(ref mut dynamic_section) = dynamic_section {
269                // link to .dynstr
270                dynamic_section.set_link(
271                    section_names
272                        .iter()
273                        .position(|name| name == ".dynstr")
274                        .expect("missing .dynstr section") as u32
275                        + 1,
276                );
277                dynamic_section.set_rel_count(rel_count);
278            }
279            current_offset += dynamic_section.size();
280
281            dynsym_section.set_offset(current_offset);
282            if let SectionType::DynSym(ref mut dynsym_section) = dynsym_section {
283                // link to .dynstr
284                dynsym_section.set_link(
285                    section_names
286                        .iter()
287                        .position(|name| name == ".dynstr")
288                        .expect("missing .dynstr section") as u32
289                        + 1,
290                );
291            }
292            current_offset += dynsym_section.size();
293
294            dynstr_section.set_offset(current_offset);
295            current_offset += dynstr_section.size();
296
297            rel_dyn_section.set_offset(current_offset);
298            if let SectionType::RelDyn(ref mut rel_dyn_section) = rel_dyn_section {
299                // link to .dynsym
300                rel_dyn_section.set_link(
301                    section_names
302                        .iter()
303                        .position(|name| name == ".dynsym")
304                        .expect("missing .dynsym section") as u32
305                        + 1,
306                );
307            }
308            current_offset += rel_dyn_section.size();
309
310            if let SectionType::Dynamic(ref mut dynamic_section) = dynamic_section {
311                dynamic_section.set_rel_offset(rel_dyn_section.offset());
312                dynamic_section.set_rel_size(rel_dyn_section.size());
313                dynamic_section.set_dynsym_offset(dynsym_section.offset());
314                dynamic_section.set_dynstr_offset(dynstr_section.offset());
315                dynamic_section.set_dynstr_size(dynstr_section.size());
316            }
317
318            // Generate debug sections
319            let debug_sections = Self::generate_debug_sections(
320                debug_sections,
321                &debug_data,
322                text_offset,
323                &mut section_names,
324                &mut current_offset,
325            );
326
327            let mut shstrtab_section = SectionType::ShStrTab(ShStrTabSection::new(
328                (section_names
329                    .iter()
330                    .map(|name| name.len() + 1)
331                    .sum::<usize>()
332                    + 1) as u32,
333                section_names,
334            ));
335            shstrtab_section.set_offset(current_offset);
336            current_offset += shstrtab_section.size();
337
338            program_headers = Some(vec![
339                ProgramHeader::new_load(
340                    text_offset,
341                    text_size,
342                    true, // executable
343                    arch,
344                ),
345                ProgramHeader::new_load(
346                    dynsym_section.offset(),
347                    dynsym_section.size() + dynstr_section.size() + rel_dyn_section.size(),
348                    false,
349                    arch,
350                ),
351                ProgramHeader::new_dynamic(dynamic_section.offset(), dynamic_section.size()),
352            ]);
353
354            sections.push(dynamic_section);
355            sections.push(dynsym_section);
356            sections.push(dynstr_section);
357            sections.push(rel_dyn_section);
358
359            for debug_section in debug_sections {
360                sections.push(debug_section);
361            }
362
363            sections.push(shstrtab_section);
364        } else {
365            // Create a vector of section names
366            let mut section_names = Vec::new();
367            for section in &sections {
368                section_names.push(section.name().to_string());
369            }
370
371            // Generate debug sections
372            let debug_sections = Self::generate_debug_sections(
373                debug_sections,
374                &debug_data,
375                text_offset,
376                &mut section_names,
377                &mut current_offset,
378            );
379
380            for debug_section in debug_sections {
381                sections.push(debug_section);
382            }
383
384            let mut shstrtab_section = ShStrTabSection::new(
385                section_names
386                    .iter()
387                    .map(|name| name.len() + 1)
388                    .sum::<usize>() as u32,
389                section_names,
390            );
391            shstrtab_section.set_offset(current_offset);
392            current_offset += shstrtab_section.size();
393            sections.push(SectionType::ShStrTab(shstrtab_section));
394        }
395
396        // Update section header offset in ELF header. v3 binaries carry no
397        // section header table unless debug info is present.
398        if !arch.is_v3() || has_debug_sections {
399            let padding = (8 - (current_offset % 8)) % 8;
400            elf_header.e_shoff = current_offset + padding;
401            elf_header.e_shnum = sections.len() as u16;
402            elf_header.e_shstrndx = sections.len() as u16 - 1;
403        }
404
405        Self {
406            elf_header,
407            program_headers,
408            sections,
409        }
410    }
411
412    pub fn emit_bytecode(&self) -> Vec<u8> {
413        let mut bytes = Vec::new();
414
415        // Emit ELF Header bytes
416        bytes.extend(self.elf_header.bytecode());
417
418        // Emit program headers
419        if let Some(program_headers) = &self.program_headers {
420            for ph in program_headers {
421                bytes.extend(ph.bytecode());
422            }
423        }
424
425        // Emit sections
426        for section in &self.sections {
427            bytes.extend(section.bytecode());
428        }
429
430        // Emit section headers (omitted when there is no section header table,
431        // e.g. v3 binaries).
432        if self.elf_header.e_shoff != 0 {
433            for section in &self.sections {
434                bytes.extend(section.section_header_bytecode());
435            }
436        }
437
438        bytes
439    }
440
441    fn generate_debug_sections(
442        parsed_debug_sections: Vec<DebugSection>,
443        debug_data: &Option<DebugData>,
444        text_offset: u64,
445        section_names: &mut Vec<String>,
446        current_offset: &mut u64,
447    ) -> Vec<SectionType> {
448        if let Some(data) = debug_data {
449            debug::generate_debug_sections(data, text_offset, section_names, current_offset)
450                .into_iter()
451                .enumerate()
452                .map(|(i, s)| match i {
453                    0 => SectionType::DebugAbbrev(s),
454                    1 => SectionType::DebugInfo(s),
455                    2 => SectionType::DebugLine(s),
456                    3 => SectionType::DebugLineStr(s),
457                    _ => unreachable!(),
458                })
459                .collect()
460        } else {
461            reuse_debug_sections(parsed_debug_sections, section_names, current_offset)
462        }
463    }
464
465    pub fn has_rodata(&self) -> bool {
466        self.sections.iter().any(|s| s.name() == ".rodata")
467    }
468
469    pub fn parse_rodata(&self) -> Vec<(String, usize, String)> {
470        let rodata = self
471            .sections
472            .iter()
473            .find(|s| s.name() == ".rodata")
474            .unwrap();
475        if let SectionType::Data(data_section) = rodata {
476            data_section.rodata()
477        } else {
478            panic!("ROData section not found");
479        }
480    }
481
482    pub fn save_to_file(&self, input_path: &str) -> std::io::Result<()> {
483        // Get the file stem (name without extension) from input path
484        let path = Path::new(input_path);
485        let file_stem = path
486            .file_stem()
487            .and_then(|s| s.to_str())
488            .unwrap_or("output");
489
490        // Create the output file name with .so extension
491        let output_path = format!("{}.so", file_stem);
492
493        // Get the bytecode
494        let bytes = self.emit_bytecode();
495
496        // Write bytes to file
497        let mut file = File::create(output_path)?;
498        file.write_all(&bytes)?;
499
500        Ok(())
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use {
507        super::*,
508        crate::{
509            SbpfArch,
510            astnode::ASTNode,
511            parser::{Token, parse},
512        },
513        sbpf_common::inst_param::Number,
514    };
515
516    #[test]
517    fn test_program_from_simple_source() {
518        let source = "exit";
519        for arch in [SbpfArch::V0, SbpfArch::V3] {
520            let parse_result = parse(source, arch).unwrap();
521            let program = Program::from_parse_result(parse_result, None);
522
523            // Verify basic structure
524            assert!(!program.sections.is_empty());
525            assert!(program.sections.len() >= 2);
526        }
527    }
528
529    #[test]
530    fn test_program_without_rodata() {
531        let source = "exit";
532        for arch in [SbpfArch::V0, SbpfArch::V3] {
533            let parse_result = parse(source, arch).unwrap();
534            let program = Program::from_parse_result(parse_result, None);
535
536            assert!(!program.has_rodata());
537        }
538    }
539
540    #[test]
541    fn test_program_emit_bytecode() {
542        let source = "exit";
543        for arch in [SbpfArch::V0, SbpfArch::V3] {
544            let parse_result = parse(source, arch).unwrap();
545            let program = Program::from_parse_result(parse_result, None);
546
547            let bytecode = program.emit_bytecode();
548            assert!(!bytecode.is_empty());
549            // Should start with ELF magic
550            assert_eq!(&bytecode[0..4], b"\x7fELF");
551        }
552    }
553
554    #[test]
555    fn test_v0_program_static_no_program_headers() {
556        // Create a static program (no dynamic symbols)
557        let source = "exit";
558        let mut parse_result = parse(source, SbpfArch::V0).unwrap();
559        parse_result.prog_is_static = true;
560
561        let program = Program::from_parse_result(parse_result, None);
562        assert!(program.program_headers.is_none());
563        assert_eq!(program.elf_header.e_phnum, 0);
564    }
565
566    #[test]
567    fn test_program_sections_ordering() {
568        let source = r"exit";
569        for arch in [SbpfArch::V0, SbpfArch::V3] {
570            let parse_result = parse(source, arch).unwrap();
571            let program = Program::from_parse_result(parse_result, None);
572
573            // First section should be null
574            assert_eq!(program.sections[0].name(), "");
575            // Second should be .text
576            assert_eq!(program.sections[1].name(), ".text");
577        }
578    }
579
580    #[test]
581    fn test_program_sections_debug() {
582        let source = "exit";
583        for arch in [SbpfArch::V0, SbpfArch::V3] {
584            let parse_result = parse(source, arch).unwrap();
585            let debug_data = Some(DebugData {
586                filename: "test.s".to_string(),
587                directory: "/test".to_string(),
588                lines: vec![],
589                labels: vec![],
590                code_start: 0,
591                code_end: 8,
592            });
593            let program = Program::from_parse_result(parse_result, debug_data);
594
595            let debug_section_names: Vec<&str> = program
596                .sections
597                .iter()
598                .map(|s| s.name())
599                .filter(|name| name.starts_with(".debug_"))
600                .collect();
601
602            assert!(debug_section_names.contains(&".debug_abbrev"));
603            assert!(debug_section_names.contains(&".debug_info"));
604            assert!(debug_section_names.contains(&".debug_line"));
605            assert!(debug_section_names.contains(&".debug_line_str"));
606        }
607    }
608
609    #[test]
610    fn test_v3_e_flags() {
611        let source = "exit";
612        let parse_result = parse(source, SbpfArch::V3).unwrap();
613        let program = Program::from_parse_result(parse_result, None);
614        assert_eq!(program.elf_header.e_flags, 3);
615    }
616
617    #[test]
618    fn test_v3_no_rodata_one_header() {
619        let source = "exit";
620        let parse_result = parse(source, SbpfArch::V3).unwrap();
621        let program = Program::from_parse_result(parse_result, None);
622
623        let headers = program.program_headers.as_ref().unwrap();
624        assert_eq!(headers.len(), 1);
625        assert_eq!(headers[0].p_flags, ProgramHeader::PF_X);
626        assert_eq!(headers[0].p_vaddr, ProgramHeader::V3_BYTECODE_VADDR);
627    }
628
629    #[test]
630    fn test_v3_with_rodata_two_headers() {
631        let source = r#"
632.rodata
633msg: .ascii "test"
634.text
635.globl entrypoint
636entrypoint:
637    exit
638        "#;
639        let parse_result = parse(source, SbpfArch::V3).unwrap();
640        let program = Program::from_parse_result(parse_result, None);
641
642        let headers = program.program_headers.as_ref().unwrap();
643        assert_eq!(headers.len(), 2);
644        // first header: rodata (PF_R, vaddr=0)
645        assert_eq!(headers[0].p_flags, ProgramHeader::PF_R);
646        assert_eq!(headers[0].p_vaddr, ProgramHeader::V3_RODATA_VADDR);
647        // second header: bytecode (PF_X, vaddr=1<<32)
648        assert_eq!(headers[1].p_flags, ProgramHeader::PF_X);
649        assert_eq!(headers[1].p_vaddr, ProgramHeader::V3_BYTECODE_VADDR);
650    }
651
652    #[test]
653    fn test_v0_with_rodata_relocations() {
654        let source = r#"
655        .rodata
656        data_0000: .byte 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11
657        data_0008: .quad data_0000
658        data_0010: .quad target_fn
659        .text
660        .globl entrypoint
661        entrypoint:
662            exit
663        target_fn:
664            exit
665        "#;
666
667        // Parse and check rodata nodes (arch v0)
668        let v0 = parse(source, SbpfArch::V0).unwrap();
669        let ASTNode::ROData { rodata, offset } = &v0.data_section.get_nodes()[1] else {
670            panic!("expected rodata node");
671        };
672        assert_eq!(rodata.name, "data_0008");
673        assert_eq!(*offset, 8);
674        assert!(matches!(
675            (&rodata.args[0], &rodata.args[1]),
676            (Token::Directive(directive, _), Token::VectorLiteral(values, _))
677                if directive == "quad"
678                    && matches!(values.as_slice(), [Number::Int(value)]
679                        if *value as u64 == (64 + 3 * 56 + 16) << 32)
680        ));
681        let ASTNode::ROData { rodata, offset } = &v0.data_section.get_nodes()[2] else {
682            panic!("expected rodata node");
683        };
684        assert_eq!(rodata.name, "data_0010");
685        assert_eq!(*offset, 16);
686        assert!(matches!(
687            (&rodata.args[0], &rodata.args[1]),
688            (Token::Directive(directive, _), Token::VectorLiteral(values, _))
689                if directive == "quad"
690                    && matches!(values.as_slice(), [Number::Int(value)]
691                        if *value as u64 == (64 + 3 * 56 + 8) << 32)
692        ));
693
694        // v0 should have 2 dynamic relocations.
695        assert_eq!(
696            v0.relocation_data.get_rel_dyns(),
697            vec![
698                (24, RelocationType::RSbf64Relative, String::new()),
699                (32, RelocationType::RSbf64Relative, String::new()),
700            ]
701        );
702
703        // Parse and check rodata nodes (arch v3)
704        let v3 = parse(source, SbpfArch::V3).unwrap();
705        let ASTNode::ROData { rodata, offset } = &v3.data_section.get_nodes()[1] else {
706            panic!("expected rodata node");
707        };
708        assert_eq!(rodata.name, "data_0008");
709        assert_eq!(*offset, 8);
710        assert!(matches!(
711            (&rodata.args[0], &rodata.args[1]),
712            (Token::Directive(directive, _), Token::VectorLiteral(values, _))
713                if directive == "quad"
714                    && matches!(values.as_slice(), [Number::Int(0)])
715        ));
716        let ASTNode::ROData { rodata, offset } = &v3.data_section.get_nodes()[2] else {
717            panic!("expected rodata node");
718        };
719        assert_eq!(rodata.name, "data_0010");
720        assert_eq!(*offset, 16);
721        assert!(matches!(
722            (&rodata.args[0], &rodata.args[1]),
723            (Token::Directive(directive, _), Token::VectorLiteral(values, _))
724                if directive == "quad"
725                    && matches!(values.as_slice(), [Number::Int(value)]
726                        if *value as u64 == ProgramHeader::V3_BYTECODE_VADDR + 8)
727        ));
728
729        // v3 should have no dynamic relocations.
730        assert!(v3.relocation_data.get_rel_dyns().is_empty());
731    }
732
733    #[test]
734    fn test_v3_e_entry() {
735        let source = r#"
736.globl entrypoint
737entrypoint:
738    exit
739        "#;
740        let parse_result = parse(source, SbpfArch::V3).unwrap();
741        let program = Program::from_parse_result(parse_result, None);
742
743        // v3: e_entry must be >= V3_BYTECODE_VADDR (1 << 32)
744        assert!(program.elf_header.e_entry >= ProgramHeader::V3_BYTECODE_VADDR,);
745    }
746
747    #[test]
748    fn test_v3_p_offset() {
749        let source = r#"
750.rodata
751msg: .ascii "test"
752.text
753.globl entrypoint
754entrypoint:
755    exit
756        "#;
757        let parse_result = parse(source, SbpfArch::V3).unwrap();
758        let program = Program::from_parse_result(parse_result, None);
759
760        let headers = program.program_headers.as_ref().unwrap();
761        let expected_first_offset = 64 + (program.elf_header.e_phnum as u64) * 56;
762        assert_eq!(headers[0].p_offset, expected_first_offset);
763        assert_eq!(
764            headers[1].p_offset,
765            headers[0].p_offset + headers[0].p_filesz
766        );
767    }
768
769    #[test]
770    fn test_v3_no_dynamic_sections() {
771        let source = r#"
772.globl entrypoint
773entrypoint:
774    call sol_log_64_
775    exit
776        "#;
777        let parse_result = parse(source, SbpfArch::V3).unwrap();
778        let program = Program::from_parse_result(parse_result, None);
779
780        // v3 should not have any dynamic sections
781        let section_names: Vec<&str> = program.sections.iter().map(|s| s.name()).collect();
782        assert!(!section_names.contains(&".dynamic"));
783        assert!(!section_names.contains(&".dynsym"));
784        assert!(!section_names.contains(&".dynstr"));
785        assert!(!section_names.contains(&".rel.dyn"));
786    }
787}