use std::fs;
use std::path::Path;
use goblin::Object;
use crate::error::Error;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Container {
Elf,
MachO,
Pe,
}
impl Container {
pub fn as_str(self) -> &'static str {
match self {
Container::Elf => "ELF",
Container::MachO => "Mach-O",
Container::Pe => "PE",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Arch {
X86_64,
Aarch64,
X86,
Arm,
Other,
}
impl Arch {
pub fn as_str(self) -> &'static str {
match self {
Arch::X86_64 => "amd64",
Arch::Aarch64 => "arm64",
Arch::X86 => "386",
Arch::Arm => "arm",
Arch::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionKind {
Text,
ReadOnlyData,
Data,
NoPtrData,
Bss,
Pclntab,
Other,
}
#[derive(Debug, Clone)]
pub struct Section {
pub name: String,
pub kind: SectionKind,
pub file_offset: usize,
pub file_size: usize,
pub addr: u64,
pub vmsize: u64,
}
impl Section {
pub fn contains_addr(&self, addr: u64) -> bool {
addr >= self.addr
&& addr
< self
.addr
.saturating_add(self.vmsize.max(self.file_size as u64))
}
pub fn file_offset_of(&self, addr: u64) -> Option<usize> {
if !self.contains_addr(addr) {
return None;
}
let delta = (addr - self.addr) as usize;
if delta >= self.file_size {
return None;
}
Some(self.file_offset.saturating_add(delta))
}
pub fn ptr_bearing(&self) -> Option<bool> {
let n = self.name.as_str();
if n.contains("noptr") {
return Some(false);
}
match self.kind {
SectionKind::Data | SectionKind::Bss => {
Some(true)
}
SectionKind::ReadOnlyData | SectionKind::NoPtrData | SectionKind::Pclntab => {
Some(false)
}
SectionKind::Text | SectionKind::Other => None,
}
}
pub fn writable(&self) -> Option<bool> {
match self.kind {
SectionKind::ReadOnlyData | SectionKind::Pclntab | SectionKind::Text => Some(false),
SectionKind::Data | SectionKind::Bss | SectionKind::NoPtrData => Some(true),
SectionKind::Other => None,
}
}
}
pub struct GoBinary {
pub bytes: Vec<u8>,
pub container: Container,
pub arch: Arch,
pub little_endian: bool,
pub ptr_size: usize,
pub sections: Vec<Section>,
pub pclntab_offset: usize,
pub pclntab_size: usize,
pub pclntab_addr: u64,
pub text_addr: u64,
}
impl GoBinary {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let bytes = fs::read(path)?;
Self::parse(bytes)
}
pub fn parse(bytes: Vec<u8>) -> Result<Self> {
if let Some(slice) = fat_slice(&bytes) {
return Self::parse(slice);
}
let parsed = describe(&bytes)?;
finish(bytes, parsed)
}
pub fn pclntab_slice(&self) -> &[u8] {
&self.bytes[self.pclntab_offset..self.pclntab_offset + self.pclntab_size]
}
pub fn pointer_size(&self) -> usize {
self.ptr_size
}
pub fn section_for_addr(&self, addr: u64) -> Option<&Section> {
self.sections.iter().find(|s| s.contains_addr(addr))
}
pub fn file_offset_for_addr(&self, addr: u64) -> Option<usize> {
self.section_for_addr(addr)
.and_then(|s| s.file_offset_of(addr))
}
pub fn read_at_addr(&self, addr: u64, len: usize) -> Option<&[u8]> {
let s = self.section_for_addr(addr)?;
let start_off = s.file_offset_of(addr)?;
let end_off = start_off.checked_add(len)?;
if end_off > s.file_offset.saturating_add(s.file_size) {
return None;
}
if end_off > self.bytes.len() {
return None;
}
Some(&self.bytes[start_off..end_off])
}
}
#[derive(Debug, Clone)]
struct Described {
container: Container,
arch: Arch,
little_endian: bool,
ptr_size: usize,
sections: Vec<Section>,
pclntab_offset: usize,
pclntab_size: usize,
pclntab_addr: u64,
text_addr: u64,
stripped_sections: bool,
}
fn fat_slice(bytes: &[u8]) -> Option<Vec<u8>> {
let fat_magic = matches!(
bytes.get(0..4),
Some([0xca, 0xfe, 0xba, 0xbe]) | Some([0xca, 0xfe, 0xba, 0xbf])
);
if !fat_magic {
return None;
}
let Ok(Object::Mach(goblin::mach::Mach::Fat(fat))) = Object::parse(bytes) else {
return None;
};
use goblin::mach::cputype::{CPU_TYPE_ARM64, CPU_TYPE_X86_64};
const MAX_FAT_ARCHES: usize = 64;
let mut best: Option<(usize, usize, u8)> = None; for arch in fat.iter_arches().take(MAX_FAT_ARCHES).flatten() {
let rank = match arch.cputype() {
CPU_TYPE_X86_64 => 0,
CPU_TYPE_ARM64 => 1,
_ => 2,
};
if best.is_none_or(|(_, _, r)| rank < r) {
best = Some((arch.offset as usize, arch.size as usize, rank));
}
}
let (off, size, _) = best?;
let end = off.checked_add(size).filter(|&e| e <= bytes.len())?;
Some(bytes[off..end].to_vec())
}
fn describe(bytes: &[u8]) -> Result<Described> {
let object = Object::parse(bytes)?;
match object {
Object::Elf(elf) => describe_elf(bytes, elf),
Object::Mach(mach) => describe_mach(bytes, mach),
Object::PE(pe) => describe_pe(bytes, pe),
_ => Err(Error::UnknownContainer),
}
}
fn describe_elf(bytes: &[u8], elf: goblin::elf::Elf<'_>) -> Result<Described> {
let arch = match elf.header.e_machine {
goblin::elf::header::EM_X86_64 => Arch::X86_64,
goblin::elf::header::EM_AARCH64 => Arch::Aarch64,
goblin::elf::header::EM_386 => Arch::X86,
goblin::elf::header::EM_ARM => Arch::Arm,
_ => Arch::Other,
};
let little_endian = elf.little_endian;
let ptr_size = if elf.is_64 { 8 } else { 4 };
let mut sections = Vec::new();
let mut text_addr = 0u64;
let mut stripped_sections = false;
let mut pcln: Option<(usize, usize, u64)> = None;
for sh in elf.section_headers.iter() {
let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("").to_string();
let kind = classify_elf_section(&name, sh);
let section = Section {
name: name.clone(),
kind,
file_offset: sh.sh_offset as usize,
file_size: sh.sh_size as usize,
addr: sh.sh_addr,
vmsize: sh.sh_size,
};
if section.kind == SectionKind::Text && text_addr == 0 {
text_addr = section.addr;
}
if matches!(name.as_str(), ".gopclntab" | "__gopclntab" | "gopclntab") {
pcln = Some((section.file_offset, section.file_size, section.addr));
}
sections.push(section);
}
if sections.is_empty() {
use goblin::elf::program_header::{PF_W, PF_X, PT_LOAD};
stripped_sections = true;
for ph in elf.program_headers.iter() {
if ph.p_type != PT_LOAD {
continue;
}
let (name, kind) = if ph.p_flags & PF_X != 0 {
(".text", SectionKind::Text)
} else if ph.p_flags & PF_W != 0 {
(".data", SectionKind::Data)
} else {
(".rodata", SectionKind::ReadOnlyData)
};
if kind == SectionKind::Text && text_addr == 0 {
text_addr = ph.p_vaddr;
}
sections.push(Section {
name: name.to_string(),
kind,
file_offset: ph.p_offset as usize,
file_size: ph.p_filesz as usize,
addr: ph.p_vaddr,
vmsize: ph.p_memsz,
});
}
}
let (pclntab_offset, pclntab_size, pclntab_addr) = match pcln {
Some(v) => v,
None => {
let (off, size) = locate_pclntab(bytes, §ions, text_addr, little_endian)?;
let addr = addr_for_offset(§ions, off).unwrap_or(0);
(off, size, addr)
}
};
Ok(Described {
container: Container::Elf,
arch,
little_endian,
ptr_size,
sections,
pclntab_offset,
pclntab_size,
pclntab_addr,
text_addr,
stripped_sections,
})
}
fn classify_elf_section(name: &str, sh: &goblin::elf::SectionHeader) -> SectionKind {
use goblin::elf::section_header::*;
if matches!(name, ".gopclntab" | "__gopclntab" | "gopclntab") {
return SectionKind::Pclntab;
}
match name {
".text" => SectionKind::Text,
".rodata" => SectionKind::ReadOnlyData,
".data" => SectionKind::Data,
".noptrdata" => SectionKind::NoPtrData,
".bss" | ".noptrbss" => SectionKind::Bss,
_ => {
if sh.sh_type == SHT_PROGBITS && (sh.sh_flags & SHF_EXECINSTR as u64) != 0 {
SectionKind::Text
} else if sh.sh_type == SHT_PROGBITS && (sh.sh_flags & SHF_WRITE as u64) != 0 {
SectionKind::Data
} else if sh.sh_type == SHT_PROGBITS {
SectionKind::ReadOnlyData
} else if sh.sh_type == SHT_NOBITS {
SectionKind::Bss
} else {
SectionKind::Other
}
}
}
}
fn describe_mach(bytes: &[u8], mach: goblin::mach::Mach<'_>) -> Result<Described> {
let macho = match mach {
goblin::mach::Mach::Binary(m) => m,
goblin::mach::Mach::Fat(fat) => {
const MAX_FAT_ARCHES: usize = 64;
let count = fat.iter_arches().take(MAX_FAT_ARCHES).count();
return Err(Error::FatBinary { slice_count: count });
}
};
let arch = match macho.header.cputype() {
goblin::mach::cputype::CPU_TYPE_X86_64 => Arch::X86_64,
goblin::mach::cputype::CPU_TYPE_ARM64 => Arch::Aarch64,
goblin::mach::cputype::CPU_TYPE_X86 => Arch::X86,
goblin::mach::cputype::CPU_TYPE_ARM => Arch::Arm,
_ => Arch::Other,
};
let little_endian = macho.little_endian;
let ptr_size = if macho.is_64 { 8 } else { 4 };
let mut sections = Vec::new();
let mut text_addr = 0u64;
let mut pcln: Option<(usize, usize, u64)> = None;
for segment in macho.segments.iter() {
let segname = segment.name().unwrap_or("").to_string();
for section in segment.sections().map_err(Error::Goblin)? {
let (sect, _data) = section;
let sectname = sect.name().unwrap_or("").to_string();
let kind = classify_mach_section(&segname, §name);
let s = Section {
name: format!("{segname},{sectname}"),
kind,
file_offset: sect.offset as usize,
file_size: sect.size as usize,
addr: sect.addr,
vmsize: sect.size,
};
if kind == SectionKind::Text && text_addr == 0 {
text_addr = s.addr;
}
if kind == SectionKind::Pclntab {
pcln = Some((s.file_offset, s.file_size, s.addr));
}
sections.push(s);
}
}
let (pclntab_offset, pclntab_size, pclntab_addr) = match pcln {
Some(v) => v,
None => {
let (off, size) = locate_pclntab(bytes, §ions, text_addr, little_endian)?;
let addr = addr_for_offset(§ions, off).unwrap_or(0);
(off, size, addr)
}
};
Ok(Described {
container: Container::MachO,
arch,
little_endian,
ptr_size,
sections,
pclntab_offset,
pclntab_size,
pclntab_addr,
text_addr,
stripped_sections: false,
})
}
fn classify_mach_section(segname: &str, sectname: &str) -> SectionKind {
if matches!(sectname, "__gopclntab" | "gopclntab") {
return SectionKind::Pclntab;
}
match (segname, sectname) {
("__TEXT", "__text") => SectionKind::Text,
("__TEXT", "__rodata") | ("__DATA_CONST", "__const") | ("__TEXT", "__const") => {
SectionKind::ReadOnlyData
}
("__DATA", "__data") => SectionKind::Data,
("__DATA", "__noptrdata") | ("__DATA", "__go_module") => SectionKind::NoPtrData,
("__DATA", "__bss") | ("__DATA", "__noptrbss") => SectionKind::Bss,
_ => SectionKind::Other,
}
}
fn describe_pe(bytes: &[u8], pe: goblin::pe::PE<'_>) -> Result<Described> {
let arch = match pe.header.coff_header.machine {
goblin::pe::header::COFF_MACHINE_X86_64 => Arch::X86_64,
goblin::pe::header::COFF_MACHINE_ARM64 => Arch::Aarch64,
goblin::pe::header::COFF_MACHINE_X86 => Arch::X86,
goblin::pe::header::COFF_MACHINE_ARM => Arch::Arm,
_ => Arch::Other,
};
let little_endian = true;
let ptr_size = if pe.is_64 { 8 } else { 4 };
let image_base = pe
.header
.optional_header
.map(|h| h.windows_fields.image_base)
.unwrap_or(0);
let mut sections = Vec::new();
let mut text_addr = 0u64;
let mut pcln: Option<(usize, usize, u64)> = None;
for sect in &pe.sections {
let name = sect.name().unwrap_or("").to_string();
let kind = classify_pe_section(&name, sect.characteristics);
let addr = image_base.saturating_add(sect.virtual_address as u64);
let s = Section {
name: name.clone(),
kind,
file_offset: sect.pointer_to_raw_data as usize,
file_size: sect.size_of_raw_data as usize,
addr,
vmsize: sect.virtual_size as u64,
};
if kind == SectionKind::Text && text_addr == 0 {
text_addr = s.addr;
}
if name == ".gopclntab" || name == "gopclntab" || name.starts_with(".gopclntab") {
pcln = Some((s.file_offset, s.file_size, s.addr));
}
sections.push(s);
}
let (pclntab_offset, pclntab_size, pclntab_addr) = match pcln {
Some(v) => v,
None => {
let (off, size) = locate_pclntab(bytes, §ions, text_addr, little_endian)?;
let addr = addr_for_offset(§ions, off).unwrap_or(0);
(off, size, addr)
}
};
Ok(Described {
container: Container::Pe,
arch,
little_endian,
ptr_size,
sections,
pclntab_offset,
pclntab_size,
pclntab_addr,
text_addr,
stripped_sections: false,
})
}
fn classify_pe_section(name: &str, characteristics: u32) -> SectionKind {
use goblin::pe::section_table::*;
const EXEC: u32 = IMAGE_SCN_MEM_EXECUTE;
const WRITE: u32 = IMAGE_SCN_MEM_WRITE;
if name == ".gopclntab" || name == "gopclntab" || name.starts_with(".gopclntab") {
return SectionKind::Pclntab;
}
match name {
".text" => SectionKind::Text,
".rdata" => SectionKind::ReadOnlyData,
".data" => SectionKind::Data,
".noptrdata" => SectionKind::NoPtrData,
".bss" | ".noptrbss" => SectionKind::Bss,
_ => {
if characteristics & EXEC != 0 {
SectionKind::Text
} else if characteristics & WRITE != 0 {
SectionKind::Data
} else {
SectionKind::ReadOnlyData
}
}
}
}
fn addr_for_offset(sections: &[Section], offset: usize) -> Option<u64> {
for s in sections {
if offset >= s.file_offset && offset < s.file_offset.saturating_add(s.file_size) {
let delta = (offset - s.file_offset) as u64;
return Some(s.addr.saturating_add(delta));
}
}
None
}
const PCLNTAB_MAGIC_1_20: [u8; 4] = [0xf1, 0xff, 0xff, 0xff];
const PCLNTAB_MAGIC_1_20_BE: [u8; 4] = [0xff, 0xff, 0xff, 0xf1];
const PCLNTAB_MAGIC_1_18: [u8; 4] = [0xf0, 0xff, 0xff, 0xff];
const PCLNTAB_MAGIC_1_18_BE: [u8; 4] = [0xff, 0xff, 0xff, 0xf0];
fn scan_for_magic(
bytes: &[u8],
sections: &[Section],
little_endian: bool,
) -> Result<(usize, usize)> {
let candidates: [[u8; 4]; 2] = if little_endian {
[PCLNTAB_MAGIC_1_20, PCLNTAB_MAGIC_1_18]
} else {
[PCLNTAB_MAGIC_1_20_BE, PCLNTAB_MAGIC_1_18_BE]
};
let mut best: Option<usize> = None;
for magic in &candidates {
let mut search_from = 0usize;
while let Some(found) = find_subslice(&bytes[search_from..], magic) {
let offset = search_from + found;
if offset + 8 > bytes.len() {
break;
}
if pcheader_at(bytes, offset, sections, little_endian).is_some() {
best = Some(best.map(|b| b.min(offset)).unwrap_or(offset));
break;
}
search_from = offset + 4;
}
}
match best {
Some(offset) => Ok((offset, bytes.len() - offset)),
None => Err(Error::NoPclntab),
}
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn locate_pclntab(
bytes: &[u8],
sections: &[Section],
text_addr: u64,
little_endian: bool,
) -> Result<(usize, usize)> {
match scan_for_magic(bytes, sections, little_endian) {
Ok(found) => Ok(found),
Err(_) => scan_for_pcheader(bytes, sections, text_addr, little_endian),
}
}
fn read_uint_at(bytes: &[u8], off: usize, ptr_size: usize, little_endian: bool) -> Option<u64> {
let slice = bytes.get(off..off.checked_add(ptr_size)?)?;
let mut v: u64 = 0;
if little_endian {
for (i, &b) in slice.iter().enumerate() {
v |= (b as u64) << (8 * i);
}
} else {
for &b in slice {
v = (v << 8) | b as u64;
}
}
Some(v)
}
fn scan_for_pcheader(
bytes: &[u8],
sections: &[Section],
_text_addr: u64,
little_endian: bool,
) -> Result<(usize, usize)> {
for s in sections {
match s.kind {
SectionKind::ReadOnlyData
| SectionKind::Data
| SectionKind::NoPtrData
| SectionKind::Pclntab
| SectionKind::Other => {}
SectionKind::Text | SectionKind::Bss => continue,
}
let start = s.file_offset.min(bytes.len());
let end = s.file_offset.saturating_add(s.file_size).min(bytes.len());
let mut off = start + if start % 8 == 0 { 0 } else { 8 - start % 8 };
while off + 8 <= end {
if let Some(size) = pcheader_at(bytes, off, sections, little_endian) {
return Ok((off, size));
}
off += 8;
}
}
Err(Error::NoPclntab)
}
fn pcheader_at(
bytes: &[u8],
off: usize,
sections: &[Section],
little_endian: bool,
) -> Option<usize> {
if bytes.get(off + 4)? != &0 || bytes.get(off + 5)? != &0 {
return None;
}
let quantum = *bytes.get(off + 6)?;
let ptr_size = *bytes.get(off + 7)?;
if !matches!(quantum, 1 | 2 | 4) || !matches!(ptr_size, 4 | 8) {
return None;
}
let ps = ptr_size as usize;
let header_end = off.checked_add(8 + 8 * ps)?;
if header_end > bytes.len() {
return None;
}
let text_start = read_uint_at(bytes, off + 8 + 2 * ps, ps, little_endian)?;
let funcname = read_uint_at(bytes, off + 8 + 3 * ps, ps, little_endian)?;
let cu = read_uint_at(bytes, off + 8 + 4 * ps, ps, little_endian)?;
let filetab = read_uint_at(bytes, off + 8 + 5 * ps, ps, little_endian)?;
let pctab = read_uint_at(bytes, off + 8 + 6 * ps, ps, little_endian)?;
let pcln = read_uint_at(bytes, off + 8 + 7 * ps, ps, little_endian)?;
if !(funcname < cu && cu < filetab && filetab < pctab && pctab < pcln) {
return None;
}
let available = (bytes.len() - off) as u64;
if pcln >= available {
return None;
}
let text_ok = text_start == 0
|| sections.iter().any(|t| {
t.kind == SectionKind::Text
&& text_start >= t.addr
&& text_start < t.addr.saturating_add(t.vmsize.max(t.file_size as u64))
});
if !text_ok {
return None;
}
Some(bytes.len() - off)
}
fn finish(bytes: Vec<u8>, d: Described) -> Result<GoBinary> {
if d.pclntab_offset >= bytes.len() || d.pclntab_offset + d.pclntab_size > bytes.len() {
return Err(Error::BadPclntab {
offset: d.pclntab_offset,
reason: format!(
"section bounds out of range (file is {} bytes)",
bytes.len()
),
});
}
let len = bytes.len();
let mut sections = d.sections;
for s in &mut sections {
s.file_offset = s.file_offset.min(len);
s.file_size = s.file_size.min(len - s.file_offset);
}
let mut bin = GoBinary {
bytes,
container: d.container,
arch: d.arch,
little_endian: d.little_endian,
ptr_size: d.ptr_size,
sections,
pclntab_offset: d.pclntab_offset,
pclntab_size: d.pclntab_size,
pclntab_addr: d.pclntab_addr,
text_addr: d.text_addr,
};
if d.stripped_sections {
if let Ok(md) = crate::moduledata::ModuleData::locate(&bin) {
if md.text != 0 {
bin.text_addr = md.text;
}
}
}
Ok(bin)
}
#[cfg(test)]
mod tests {
use super::*;
fn sec(name: &str, kind: SectionKind) -> Section {
Section {
name: name.to_string(),
kind,
file_offset: 0,
file_size: 1,
addr: 0x1000,
vmsize: 1,
}
}
#[test]
fn read_at_addr_rejects_overflowing_section_range() {
let bin = GoBinary {
bytes: vec![0u8; 64],
container: Container::Elf,
arch: Arch::X86_64,
little_endian: true,
ptr_size: 8,
sections: vec![Section {
name: ".text".to_string(),
kind: SectionKind::Text,
file_offset: usize::MAX - 16,
file_size: usize::MAX,
addr: 0x1000,
vmsize: usize::MAX as u64,
}],
pclntab_offset: 0,
pclntab_size: 0,
pclntab_addr: 0,
text_addr: 0x1000,
};
assert_eq!(bin.read_at_addr(0x1000, 16), None);
assert_eq!(bin.read_at_addr(0x1008, 8), None);
}
#[test]
fn finish_clamps_section_file_range_to_input_length() {
let d = Described {
container: Container::Elf,
arch: Arch::X86_64,
little_endian: true,
ptr_size: 8,
sections: vec![Section {
name: ".text".to_string(),
kind: SectionKind::Text,
file_offset: usize::MAX - 8,
file_size: usize::MAX,
addr: 0x1000,
vmsize: 1,
}],
pclntab_offset: 0,
pclntab_size: 0,
pclntab_addr: 0,
text_addr: 0x1000,
stripped_sections: false,
};
let bin = finish(vec![0u8; 100], d).expect("finish");
let s = &bin.sections[0];
assert!(s.file_offset <= 100);
assert!(s.file_offset + s.file_size <= 100);
let _ = &bin.bytes[s.file_offset..s.file_offset + s.file_size];
}
#[test]
fn ptr_bearing_classifies_go_sections_by_name_first() {
assert_eq!(
sec(".noptrdata", SectionKind::NoPtrData).ptr_bearing(),
Some(false)
);
assert_eq!(
sec(".noptrbss", SectionKind::Bss).ptr_bearing(),
Some(false)
);
assert_eq!(sec(".data", SectionKind::Data).ptr_bearing(), Some(true));
assert_eq!(sec(".bss", SectionKind::Bss).ptr_bearing(), Some(true));
assert_eq!(
sec(".rodata", SectionKind::ReadOnlyData).ptr_bearing(),
Some(false)
);
assert_eq!(
sec(".gopclntab", SectionKind::Pclntab).ptr_bearing(),
Some(false)
);
assert_eq!(sec(".text", SectionKind::Text).ptr_bearing(), None);
assert_eq!(sec(".shstrtab", SectionKind::Other).ptr_bearing(), None);
}
#[test]
fn writable_matches_runtime_protection_bits() {
assert_eq!(
sec(".rodata", SectionKind::ReadOnlyData).writable(),
Some(false)
);
assert_eq!(sec(".text", SectionKind::Text).writable(), Some(false));
assert_eq!(
sec(".gopclntab", SectionKind::Pclntab).writable(),
Some(false)
);
assert_eq!(sec(".data", SectionKind::Data).writable(), Some(true));
assert_eq!(sec(".bss", SectionKind::Bss).writable(), Some(true));
assert_eq!(
sec(".noptrdata", SectionKind::NoPtrData).writable(),
Some(true)
);
assert_eq!(sec(".shstrtab", SectionKind::Other).writable(), None);
}
}